/* Image Studio and its greyed-out twin, Video Studio. */

.studio__state {
  margin-bottom: var(--sp-5);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  font-size: var(--step--1);
  border: 1px solid var(--line);
  background: var(--surface);
}
.studio__state[data-kind='off'] {
  border-color: color-mix(in srgb, var(--warn) 40%, var(--line));
  background: color-mix(in srgb, var(--warn) 8%, transparent);
}
.studio__state[data-kind='lock'] {
  border-color: var(--accent-line);
  background: var(--accent-soft);
}

/* --------------------------------------------------------------- presets -- */
/* A row, not a grid: these are examples to skim, and the horizontal scroll is
   contained so the page itself never moves sideways. */

.presets {
  display: flex;
  gap: var(--sp-3);
  margin-bottom: var(--sp-5);
  padding-bottom: var(--sp-2);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
}

.preset {
  flex: 0 0 auto;
  scroll-snap-align: start;
  display: grid;
  gap: var(--sp-1);
  width: 12.5rem;
  padding: var(--sp-4);
  text-align: left;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background: var(--surface);
  transition: border-color var(--t-fast) var(--ease-out),
              background var(--t-fast) var(--ease-out),
              transform var(--t-fast) var(--ease-out);
}
.preset:hover { border-color: var(--accent-line); background: var(--surface-2); }
.preset:active { transform: scale(0.985); }

.preset__title {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: var(--track-title);
}
.preset__prompt { font-size: var(--step--1); }

.preset--ghost { opacity: 0.42; pointer-events: none; }

/* ------------------------------------------------------------------ grid -- */

.studio__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: var(--sp-5);
  align-items: start;
}

.drop {
  display: grid;
  place-items: center;
  min-height: 18rem;
  padding: var(--sp-5);
  border-radius: var(--r-lg);
  border: 1px dashed var(--line-strong);
  background: var(--surface);
  color: var(--text-muted);
  text-align: center;
  overflow: hidden;
  transition: border-color var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out);
}
.drop:hover, .drop.is-over { border-color: var(--accent); background: var(--accent-soft); }
.drop__empty { display: grid; justify-items: center; gap: var(--sp-2); }
.drop__empty svg { color: var(--accent); }
.drop__img { max-height: 22rem; border-radius: var(--r-md); }

.drop--off { pointer-events: none; opacity: 0.45; filter: saturate(0.2); }

.studio__panel { display: grid; gap: var(--sp-3); }
.studio__prompt { min-height: 5.5rem; padding: var(--sp-3) var(--sp-4); resize: vertical; line-height: 1.5; }
.studio__go { width: 100%; }
.studio__hint { font-size: var(--step--1); min-height: 1.2em; }

.studio__progress { display: grid; gap: var(--sp-2); }

.studio__result {
  margin: var(--sp-6) 0 0;
  display: grid;
  gap: var(--sp-3);
}
.studio__result img { border-radius: var(--r-lg); border: 1px solid var(--line); }
.studio__result-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  flex-wrap: wrap;
}
.studio__result-actions { display: flex; gap: var(--sp-2); }

/* The whole screen reads as switched off, not merely empty. */
.studio--off .page__head, .studio--off .studio__grid { filter: saturate(0.35); }

@media (max-width: 860px) {
  .studio__grid { grid-template-columns: minmax(0, 1fr); }
  .preset { width: 11rem; }
}

/* ------------------------------------------------ image model chooser -- */

.studio__picker { margin-bottom: var(--sp-3); }
.studio__head .studio__picker { margin-bottom: 0; }

/* The menu drops DOWN here, unlike chat's, because this picker sits mid-panel
   with room below rather than pinned above a composer. */
.studio__picker .picker__menu {
  right: auto;
  left: 0;
  bottom: auto;
  top: calc(100% + var(--sp-2));
  transform-origin: top left;
}

/* Superseded versions live one level in. Opening to the right keeps the
   recommended models in place instead of pushing them around as the list grows. */
.picker__submenu { position: relative; }

.picker__item--parent { width: 100%; }

.picker__sub {
  position: absolute;
  left: calc(100% + var(--sp-1));
  top: calc(var(--sp-1) * -1);
  width: max(200px, 100%);
  padding: var(--sp-2);
  display: none;
  gap: 2px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  z-index: 21;
}
.picker__submenu.is-open .picker__sub { display: grid; }

/* On a narrow screen there is no room to the right, so it becomes a nested
   list rather than a flyout that would hang off the edge. */
@media (max-width: 560px) {
  .picker__sub {
    position: static;
    width: auto;
    margin-top: 2px;
    border: 0;
    box-shadow: none;
    padding: 0 0 0 var(--sp-3);
    border-left: 1px solid var(--line);
    border-radius: 0;
  }
}

/* ------------------------------------------------------- preset previews -- */
/*
 * Each card shows the same flower twice, split down the middle: left as it
 * arrived, right after the edit. A label can say "czarno-białe"; only the
 * picture says what that will look like.
 */

.shot {
  position: relative;
  display: block;
  aspect-ratio: 16 / 10;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--surface-2);
}
.shot__half { position: absolute; inset: 0; }
.shot__half--after { clip-path: inset(0 0 0 50%); }
.shot__art { width: 100%; height: 100%; display: block; object-fit: cover; }

/* The seam is what makes it read as one image cut in half rather than two
   pictures placed side by side. */
.shot__seam {
  position: absolute;
  inset: 0 auto 0 50%;
  width: 1px;
  background: color-mix(in srgb, var(--bg) 55%, transparent);
}

/* A wash sits over the treated half for effects a filter alone cannot suggest —
   snow, warm light, paper. */
.shot__wash { position: absolute; inset: 0; }

.shot[data-look='mono'] .shot__half--after .shot__art { filter: grayscale(1) contrast(1.06); }

.shot[data-look='pencil'] .shot__half--after .shot__art {
  filter: grayscale(1) contrast(1.75) brightness(1.22);
}
.shot[data-look='pencil'] .shot__wash {
  background: repeating-linear-gradient(
    52deg, transparent 0 3px, color-mix(in srgb, #2a2a27 22%, transparent) 3px 4px);
  mix-blend-mode: multiply;
}

.shot[data-look='winter'] .shot__half--after .shot__art {
  filter: saturate(0.45) brightness(1.16) hue-rotate(165deg);
}
.shot[data-look='winter'] .shot__wash {
  background:
    radial-gradient(circle at 22% 24%, #fff 0 1.6px, transparent 2px),
    radial-gradient(circle at 68% 16%, #fff 0 1.9px, transparent 2.4px),
    radial-gradient(circle at 44% 58%, #fff 0 1.5px, transparent 2px),
    radial-gradient(circle at 82% 72%, #fff 0 1.8px, transparent 2.3px),
    radial-gradient(circle at 32% 84%, #fff 0 1.4px, transparent 1.9px),
    radial-gradient(circle at 60% 38%, #fff 0 1.3px, transparent 1.8px);
}

.shot[data-look='old'] .shot__half--after .shot__art {
  filter: sepia(0.8) contrast(0.9) brightness(1.04) saturate(0.85);
}
.shot[data-look='old'] .shot__wash {
  background: radial-gradient(ellipse at center, transparent 42%, rgba(60, 40, 18, 0.42) 100%);
}

.shot[data-look='bright'] .shot__half--after .shot__art {
  filter: brightness(1.42) saturate(1.12);
}

.shot[data-look='dusk'] .shot__half--after .shot__art {
  filter: sepia(0.32) saturate(1.5) hue-rotate(-16deg) brightness(1.04);
}
.shot[data-look='dusk'] .shot__wash {
  background: linear-gradient(200deg,
    color-mix(in srgb, var(--bursztyn) 44%, transparent),
    transparent 62%);
}

.presets__note { margin: calc(var(--sp-3) * -1) 0 var(--sp-5); font-size: var(--step--1); }

/* The card grows a picture, so it stops being a text chip. */
.preset {
  width: 13.5rem;
  padding: var(--sp-2);
  gap: var(--sp-2);
}
.preset__text { display: grid; gap: 1px; padding: 0 var(--sp-2) var(--sp-1); }
.preset__title { font-size: var(--step-0); }
.preset--ghost .shot { filter: grayscale(1); opacity: 0.5; }

/* --------------------------------------------------------- dead versions -- */
/* A version with no checkpoint is listed but not offered: it stays visible so
   the family history reads, and unselectable so it cannot waste a click. */
.picker__dot[data-state='dead'] {
  background: none;
  border: 1px solid var(--text-faint);
  box-shadow: none;
}

/* ------------------------------------------------------------- structure -- */
/*
 * The screen is two steps, and says so. Before, it was a wall of controls in
 * which the upload — the one thing nothing else works without — looked exactly
 * as important as a prompt suggestion.
 */

/* Title and version chooser sit on one line, centred against each other. They
   were bottom-aligned, which put the picker below the cap height of the heading
   and read as two things that had drifted apart rather than a pair. */
/* The version pill lines up with the title, not with the middle of the
   title-plus-back stack - the title is the thing it qualifies. */
.studio__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
  margin-bottom: var(--sp-6);
}
.studio__head .label { display: block; margin-bottom: var(--sp-2); }

.studio__step { margin-bottom: var(--sp-7); }

.studio__step-title {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: var(--track-title);
}
/* Numbered because the order is real: there is nothing to run until there is a
   picture. Numbering that encodes nothing would be decoration. */
.studio__step-n {
  display: grid;
  place-items: center;
  width: 24px; height: 24px;
  flex: none;
  border-radius: var(--r-pill);
  border: 1px solid var(--accent-line);
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 0.72rem;
}

/* The drop zone leads, because it gates everything after it. */
.studio__grid { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); }

.drop { position: relative; min-height: 21rem; }
.drop__swap {
  position: absolute;
  right: var(--sp-3);
  bottom: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-pill);
  border: 1px solid var(--line-strong);
  background: var(--glass);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--text);
  font-size: var(--step--1);
}
.drop__swap:hover { background: var(--surface-3); }

.studio__panel { align-content: start; }
.studio__go { min-height: 46px; }

/* The result is an arrival, so it gets a heading above it rather than a caption
   underneath — you read what it is, then see it. */
.studio__result { margin-top: var(--sp-6); }
.studio__result-foot { margin-bottom: var(--sp-3); }

@media (max-width: 860px) {
  .studio__head { align-items: flex-start; }
  .studio__grid { grid-template-columns: minmax(0, 1fr); }
  .drop { min-height: 15rem; }
}

/* A real preview is a photograph, so it fills its half rather than sitting in
   it like a drawing does. */
.shot--real .shot__art { width: 100%; height: 100%; object-fit: cover; }


/* Taking the picture back out is as reachable as putting it in. */
.drop__clear {
  position: absolute;
  top: var(--sp-3);
  right: var(--sp-3);
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line-strong);
  background: var(--glass);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--text);
}
.drop__clear svg { width: 14px; height: 14px; }
.drop__clear:hover { background: var(--surface-3); }

/* A card whose preview is a drawing rather than model output. Marked so the
   note above the row is not the only thing distinguishing them. */
.preset--drawn .shot::after {
  content: 'poglądowe';
  position: absolute;
  right: 4px; bottom: 4px;
  padding: 1px 6px;
  border-radius: var(--r-pill);
  background: var(--glass);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  font-family: var(--font-mono);
  font-size: 0.56rem;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}
/* The lede sat adrift: the title already carries a bottom margin and the
   paragraph added its own default on top, opening a gap wide enough to read as
   a missing element. It belongs directly under the heading it explains. */
.doodle__lede {
  margin: calc(-1 * var(--sp-2)) 0 var(--sp-5);
  max-width: 52ch;
  font-size: var(--step--1);
}

.doodle__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--sp-5);
  align-items: start;
}

.doodle__stage {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  border: 1px solid var(--line);
  border-radius: var(--r-lg, 14px);
  background: var(--surface-2);
  overflow: hidden;
}

/* The canvas inherits `color` so the stroke follows the theme without the
   painter needing to know which theme is on. */
.doodle__canvas {
  width: 100%;
  height: 100%;
  color: var(--text);
  touch-action: none;
}

.doodle__placeholder {
  position: absolute;
  pointer-events: none;
  font-size: var(--step--1);
}

.doodle__panel { display: flex; flex-direction: column; gap: var(--sp-3); }

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

.doodle__chip {
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--text-muted);
  font-size: var(--step--1);
  cursor: pointer;
  transition: border-color .15s, color .15s;
}
.doodle__chip:hover { border-color: var(--accent-line); color: var(--accent); }

.doodle__go { align-self: flex-start; }

.doodle__status { min-height: 2.6em; font-size: var(--step--1); }
.doodle__status[data-kind='ok'] { color: var(--accent); }
.doodle__status[data-kind='warn'] { color: var(--text); }

@media (max-width: 760px) {
  .doodle__grid { grid-template-columns: minmax(0, 1fr); }
}

/* The model answering in words. It stays until dismissed, because the list of
   edits it knows is the useful part and a toast took it away mid-read. */
.studio__said {
  display: grid;
  gap: var(--sp-3);
  justify-items: start;
  margin-top: var(--sp-3);
  padding: var(--sp-4);
  border-radius: var(--r-md);
  border: 1px solid var(--line-strong);
  background: var(--surface-2);
  font-size: var(--step--1);
  line-height: 1.6;
}
.studio__said p { white-space: pre-wrap; }

/* The model's whole vocabulary, as buttons. Folded away by default so it does
   not shout over the prompt, but one click from anyone who guessed wrong. */
.vocab { margin-top: var(--sp-3); }
.vocab summary { cursor: pointer; }
.vocab__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
}
.vocab__chip {
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--text-1);
  font: inherit;
  font-size: var(--step--2);
  cursor: pointer;
  transition: border-color .15s ease, background .15s ease;
}
.vocab__chip:hover { border-color: var(--accent); background: var(--surface-3); }
@media (prefers-reduced-motion: reduce) { .vocab__chip { transition: none; } }

/* ------------------------------------------------------------- chooser -- */
/*
 * The landing screen is now one sentence and an arrow at the pill. Everything
 * that used to be here - three cards, three dots, three taglines - moved into
 * the pill, which is on every screen; keeping a second copy on this one would
 * have been two controls for one job, and the copy that only exists on the
 * landing screen is the one nobody would ever learn.
 */
.studio__where { display: grid; gap: var(--sp-2); justify-items: start; }

.studio__back {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  /* The chevron hangs into the margin so the label - the part you read - lines
     up with the title under it rather than sitting one glyph to its right. */
  margin-left: -3px;
  padding: 4px 2px;
  border: 0;
  background: none;
  color: var(--text-muted);
  font-size: var(--step--1);
  letter-spacing: var(--track-body);
  cursor: pointer;
}
.studio__back:hover { color: var(--text); }
.studio__back svg { transition: transform var(--t-fast) var(--ease-out); }
.studio__back:hover svg { transform: translateX(-2px); }

.chooser {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(var(--sp-5), 5vh, var(--sp-7));
  min-height: clamp(22rem, 66vh, 46rem);
  text-align: center;
}

/* As big as the space allows. It is the only thing on the screen, so anything
   less than enormous would read as a heading waiting for content under it. */
.chooser__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(3rem, 11vw, 9rem);
  font-weight: 600;
  letter-spacing: -0.045em;
  line-height: 0.94;
}

.chooser__point { color: var(--text-faint); }
.chooser__point svg { width: 22px; height: clamp(40px, 7vh, 64px); display: block; }
/* Drawn once on arrival, then a slow drift down toward the thing it points at.
   Slow enough to be noticed only after the words have been read. */
.chooser__point-stem { stroke-dasharray: 52; stroke-dashoffset: 52; animation: point-draw 700ms var(--ease-out) 380ms forwards; }
.chooser__point-head { opacity: 0; animation: point-head 400ms var(--ease-out) 1000ms forwards; }
.chooser__point { animation: point-drift 2.8s ease-in-out 1.4s infinite; }

@keyframes point-draw { to { stroke-dashoffset: 0; } }
@keyframes point-head { to { opacity: 1; } }
@keyframes point-drift {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(6px); }
}
@media (prefers-reduced-motion: reduce) {
  .chooser__point, .chooser__point-stem, .chooser__point-head { animation: none; }
  .chooser__point-stem { stroke-dashoffset: 0; }
  .chooser__point-head { opacity: 1; }
}

/* --------------------------------------------------------- the pill nav -- */
/*
 * Floating chrome, so it is a material with the page passing under it rather
 * than a bar that eats a strip of the screen. Fixed to the viewport and centred
 * on the content column, present on every screen in Image Studio.
 */
/* Centred on the content, not on the window. Fixed positioning measures from
   the viewport, which includes the sidebar - so "middle" landed a sidebar's
   half-width left of the middle of the page it belongs to. The left edge is
   inset by whatever the sidebar currently is, and it follows the collapse
   because it reads the same custom property the grid does. */
#nav-host {
  position: fixed;
  inset: auto 0 0 var(--sidebar-w);
  z-index: 40;
  pointer-events: none;
  transition: left var(--t-base) var(--ease-settle);
}
.app[data-sidebar='collapsed'] #nav-host { left: var(--sidebar-w-collapsed); }

/* On a phone the sidebar is a drawer over the page rather than a column beside
   it, so there is nothing to inset past. */
@media (max-width: 860px) {
  #nav-host { left: 0; }
  .app[data-sidebar='collapsed'] #nav-host { left: 0; }
}

.pillnav {
  pointer-events: auto;
  width: max-content;
  max-width: calc(100vw - var(--sp-6));
  margin: 0 auto;
  margin-bottom: calc(var(--sp-5) + env(safe-area-inset-bottom));
  padding: 4px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, white 12%, transparent);
  background: color-mix(in srgb, var(--surface) 62%, transparent);
  backdrop-filter: blur(22px) saturate(170%);
  box-shadow: var(--shade-1-up), var(--shade-2-up);
}

.pillnav__track {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--pn-count, 3), minmax(0, 1fr));
  touch-action: pan-y;                 /* the pill owns horizontal, the page vertical */
  cursor: grab;
  /* The thumb is kept inside its own pill. A loose spring overshoots by design
     and the velocity stretch grows from the centre, so at either end the far
     half escaped - measured at 73px past the right edge, sitting on top of the
     label next to it. Clipped, the same overshoot reads as the thumb pressing
     against the end of the track, which is what it physically is. The radius
     has to match the pill or the corners cut square. */
  overflow: hidden;
  border-radius: inherit;
}
.pillnav__track[data-dragging] { cursor: grabbing; }

/* One thumb that travels, rather than three highlights that blink on and off:
   the three models are three positions of one thing, and the travel is what
   says so. */
.pillnav__thumb {
  position: absolute;
  inset: 0 auto 0 0;
  width: calc(100% / var(--pn-count, 3));
  border-radius: 999px;
  background: var(--text);
  transform-origin: center;
  will-change: transform;
}

/* 44px tall on touch, which is Apple's floor and not a suggestion. This is the
   only way to change model on a phone, and it was 34. */
.pillnav__slot {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: 38px;
  padding: var(--sp-2) clamp(var(--sp-3), 2.4vw, var(--sp-5));
  border: 0;
  background: none;
  border-radius: 999px;
  cursor: inherit;
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: var(--track-body);
  white-space: nowrap;
  /* --on is written per frame from the thumb's live position, so the label
     inverts as the thumb actually arrives under it - including mid-drag, where
     there is no selected value yet. */
  color: color-mix(in srgb, var(--bg) calc(var(--on, 0) * 100%),
                   color-mix(in srgb, var(--text) 68%, transparent));
}
.pillnav__slot:hover { color: color-mix(in srgb, var(--bg) calc(var(--on, 0) * 100%), var(--text)); }

.pillnav__dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: color-mix(in srgb, currentColor 40%, transparent);
  flex: none;
}
.pillnav__dot[data-state='online'] { background: var(--sitowie, #8c9b5a); }

/* The pill floats over the page, so every screen here needs room under it. */
.studio { padding-bottom: calc(var(--sp-8) + 3rem); }

/* --------------------------------------------------- the version slider -- */
/*
 * Versions are a ladder, so the control is a ladder. A dropdown flattened them
 * into three unrelated things you pick one of; left to right is 1, 2, 2.1, and
 * moving along it is a move rather than a choice.
 */
.studio__version { display: grid; gap: var(--sp-2); justify-items: end; }

.vslider {
  display: grid;
  gap: var(--sp-2);
  justify-items: end;
  width: clamp(9rem, 22vw, 13rem);
  cursor: grab;
}
.vslider[data-dragging] { cursor: grabbing; }

.vslider__channel {
  position: relative;
  width: 100%;
  height: 34px;
  /* The visual groove stays 34px; the grabbable area does not. See the touch
     rule below. */
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface-2);
  overflow: hidden;
  touch-action: pan-y;
}

.vslider__fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}

/* The rungs, so the ladder is visible before it is touched. */
.vslider__tick {
  position: absolute;
  top: 50%;
  width: 3px; height: 3px;
  margin-top: -1.5px;
  /* Centred on its own position, so a rung sits under the middle of the thumb
     rather than starting there. */
  transform: translateX(-50%);
  border-radius: 50%;
  background: color-mix(in srgb, var(--text) 26%, transparent);
}

.vslider__thumb {
  position: absolute;
  top: 3px; left: 3px;
  width: 26px; height: 26px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: var(--text);
  color: var(--bg);
  font-size: 0.68rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  transform-origin: center;
  will-change: transform;
  transition: width 220ms var(--ease-out), height 220ms var(--ease-out);
}
.vslider__thumb[data-big='true'] { width: 30px; height: 30px; top: 1px; }

.vslider__caption {
  font-size: 0.68rem;
  line-height: 1.3;
  text-align: right;
  max-width: 100%;
}

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

/* On a phone the pill is the primary control and the slider follows the title
   rather than hugging the right edge of a column that is barely wider. */
@media (max-width: 620px) {
  .studio__version, .vslider { justify-items: stretch; }
  .vslider { width: 100%; }
  .vslider__caption { text-align: left; }
}

/* ------------------------------------------------------------- g-weird -- */
/*
 * An empty screen you write into. The only rule here is that nothing looks like
 * a form: no field border, no label, no button. What you have written is the
 * interface, and the queue is the only furniture that ever appears.
 */
.weird {
  display: flex;
  flex-direction: column;
  min-height: min(70vh, 640px);
}

.weird__stage {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-3);
  padding: var(--sp-6) 0;
  cursor: text;
}

/* inline-block, not inline: the outline drawn on Enter is measured from this
   box, and an inline box that wraps reports the union of its line boxes rather
   than the shape you can see. */
.weird__line {
  display: inline-block;
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 4.4vw, 3rem);
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: -0.015em;
  max-width: 20ch;
  overflow-wrap: anywhere;
}

/* The caret is ours, because the real input is invisible. It sits on the text
   baseline and blinks on the same rhythm a native one does. */
.weird__caret {
  display: inline-block;
  width: 2px;
  height: 1em;
  margin-left: 1px;
  vertical-align: -0.12em;
  background: var(--accent);
  animation: weird-blink 1.05s steps(1, end) infinite;
}
@keyframes weird-blink { 0%, 45% { opacity: 1; } 50%, 95% { opacity: 0; } }

.weird__hint { font-size: var(--step--1); letter-spacing: 0.01em; }

/* Present, focusable, and carrying the keyboard - just not visible. Not
   display:none and not hidden: either would take the mobile keyboard with it. */
.weird__ghost {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 1px; height: 1px;
  border: 0; padding: 0;
}

/* The prompt in flight. Three beats, each with its own timing, all on one
   element: the outline closes into a ball, the letters run to the middle of it,
   and the ball is thrown at the tile. Fixed-position so it travels over the page
   rather than inside it. */
.weird__flight {
  position: fixed;
  z-index: 60;
  border-radius: var(--r-md);
  border: 1px solid transparent;
  overflow: hidden;
  pointer-events: none;
  transition:
    left var(--t-enter) cubic-bezier(0.5, 0, 0.2, 1),
    top var(--t-enter) cubic-bezier(0.5, 0, 0.2, 1),
    width var(--t-enter) cubic-bezier(0.5, 0, 0.2, 1),
    height var(--t-enter) cubic-bezier(0.5, 0, 0.2, 1),
    border-radius var(--t-enter) cubic-bezier(0.5, 0, 0.2, 1),
    border-color 200ms var(--ease-out),
    background var(--t-enter) var(--ease-out);
}

/* Beat one: the line, drawn the size of what was written. */
.weird__flight[data-phase='outline'] {
  border-color: var(--accent-line, var(--accent));
  background: color-mix(in srgb, var(--accent) 7%, transparent);
}

/* Beat two: a ball. */
.weird__flight[data-phase='gather'] {
  border-radius: 50%;
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 22%, var(--surface-2));
}

/* Beat three: thrown. Uniform scale on a round thing, so there is nothing left
   to distort - which is the whole reason the text gathered first. */
.weird__flight[data-phase='throw'] {
  border-radius: 50%;
  transition:
    transform 440ms cubic-bezier(0.35, 0, 0.25, 1),
    opacity var(--t-fast) ease var(--t-enter),
    background 440ms var(--ease-out);
  background: color-mix(in srgb, var(--accent) 34%, var(--surface-2));
  opacity: 0;
}

/* Each letter keeps its place until it runs for the middle - the arrival
   animation played backwards, blur and all. */
.weird__flight-char {
  position: absolute;
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 4.4vw, 3rem);
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: -0.015em;
  color: var(--text);
  white-space: pre;
  transition:
    transform 260ms cubic-bezier(0.5, 0, 0.75, 0),
    opacity 220ms ease,
    filter 220ms ease;
}
.weird__flight[data-phase='gather'] .weird__flight-char,
.weird__flight[data-phase='throw'] .weird__flight-char {
  transform: translate(var(--dx), var(--dy)) scale(0.25);
  filter: blur(6px);
  opacity: 0;
}

/* ------------------------------------------------------- g-weird queue -- */
/* A shelf the tiles land on. The edge is a fading gradient rather than a 1px
   rule: a hard divider under floating chrome announces a boundary, a fade just
   says the page continues underneath. */
.weird__queue {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  padding-top: var(--sp-5);
  min-height: 96px;
  align-content: flex-start;
}
.weird__queue::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg,
    transparent, var(--line-strong) 12%, var(--line-strong) 88%, transparent);
  opacity: 0.75;
}
.weird__queue:empty::before { opacity: 0; }

.weird__tile {
  position: relative;
  width: 84px; height: 84px;
  border-radius: 18px;
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--line);
  cursor: pointer;
  flex: none;
  box-shadow: var(--shade-1), var(--shade-2);
  transition: transform var(--t-fast) cubic-bezier(0.32, 0.72, 0, 1),
              box-shadow var(--t-fast) var(--ease-out);
}
/* Lifts toward the pointer, and takes the press. Only once it has something to
   show - lifting a tile that is still painting promises a picture. */
.weird__tile[data-status='done']:hover {
  transform: translateY(-3px) scale(1.03);
  box-shadow: var(--shade-1-up), var(--shade-2-up);
}
.weird__tile[data-status='done']:active { transform: translateY(-1px) scale(0.99); }
.weird__tile[data-status='pending'] { cursor: default; }

/* The tile takes the hit, so the ball is felt landing rather than just ceasing
   to exist somewhere near it. */
.weird__tile[data-landed] { animation: weird-land 340ms var(--ease-out); }
@keyframes weird-land {
  0% { transform: scale(0.82); }
  55% { transform: scale(1.06); }
  100% { transform: scale(1); }
}

/* While the Mac paints there is nothing to show, so the tile shows movement
   instead of a number: a gradient sliding under a spinner. It is honest about
   being indeterminate, which a progress bar would not be - nothing reports
   progress from inside the sampling loop. */
.weird__tile-shimmer {
  position: absolute;
  inset: 0;
  background: linear-gradient(115deg,
    var(--surface-2) 0%, var(--accent-soft, var(--surface-3)) 45%,
    var(--surface-3) 60%, var(--surface-2) 100%);
  background-size: 300% 100%;
  animation: weird-shift 2.4s linear infinite;
}
.weird__tile[data-status='done'] .weird__tile-shimmer { opacity: 0; }
@keyframes weird-shift {
  from { background-position: 0% 50%; }
  to { background-position: 300% 50%; }
}

/* No spinner. The tile is already the shape of the thing being made and the
   gradient sliding under it is already saying "working" - a spinning circle on
   top of a skeleton is two loading indicators for one wait, and the generic one
   is the one that tells you least. What is left reads as the picture developing
   rather than as a widget. */

/* A picture that never arrived says so on the tile itself. A title attribute is
   not a state: it needs a hover, it never appears on a phone, and a tile that
   simply stops moving looks like one still working. */
.weird__tile[data-status='error']::after,
.weird__tile[data-status='cancelled']::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(45deg, transparent calc(50% - 1px),
      color-mix(in srgb, var(--text) 34%, transparent) 50%,
      transparent calc(50% + 1px));
}

.weird__tile-img {
  position: relative;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  transform: scale(1.04);
  transition: opacity 520ms var(--ease-out), transform 520ms var(--ease-out);
}
/* The reveal is lit from underneath by the picture's own colours, sampled once
   it has loaded - so what arrives is this image appearing, not a stock flourish
   playing over it. */
.weird__tile[data-revealed='true'] {
  background: linear-gradient(135deg, var(--tile-a), var(--tile-b));
}
.weird__tile[data-revealed='true'] .weird__tile-img { opacity: 1; transform: scale(1); }

/* Visible without a hover, and big enough to hit. An iPhone has no hover, so
   this was an invisible-but-hit-testable control in the corner of a tile whose
   whole surface opens the picture: you could not cancel a paint, or you cancelled
   one by accident with nothing on screen to explain why. The ::before gives it a
   44px target without growing the 20px dot. */
.weird__tile-stop {
  position: absolute;
  top: 3px; right: 3px;
  width: 20px; height: 20px;
  display: grid; place-items: center;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, black 55%, transparent);
  color: white;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease-out);
}
.weird__tile:hover .weird__tile-stop,
.weird__tile-stop:focus-visible { opacity: 1; }
.weird__tile-stop::before { content: ''; position: absolute; inset: -12px; }
@media (hover: none) {
  .weird__tile-stop { opacity: 0.85; }
  /* Fingers, not pointers. Both of these are primary controls and both sat
     under the 44px minimum - the pill is the only way to change model on a
     phone, and the slider the only way to change version. */
  .pillnav__slot { min-height: 44px; }
  .vslider__channel { height: 44px; }
  /* Grown with the channel so the inset stays 3px at both ends - the travel
     maths in version-slider.js reads offsetWidth, so it follows on its own. */
  .vslider__thumb { width: 38px; height: 38px; }
  .vslider__thumb[data-big='true'] { width: 38px; height: 38px; top: 3px; }
}

.weird__tile[data-status='error'],
.weird__tile[data-status='cancelled'] { border-style: dashed; opacity: 0.6; }

/* ------------------------------------------------------ g-weird viewer -- */
/* The scrim materialises rather than fading: blur and tint arrive together, so
   it reads as glass sliding in front of the page instead of a grey sheet. */
.weird__viewer {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  gap: var(--sp-5);
  padding: var(--sp-6);
  background: color-mix(in srgb, var(--bg) 82%, black);
  backdrop-filter: blur(0px) saturate(120%);
  transition: backdrop-filter 320ms var(--ease-out), background 320ms var(--ease-out);
}
.weird__viewer[data-open='true'] { backdrop-filter: blur(22px) saturate(140%); }
.weird__viewer[data-open='false'] { backdrop-filter: blur(0px); background: transparent; }
.weird__viewer-figure {
  display: grid;
  gap: var(--sp-4);
  justify-items: center;
  margin: 0;
  /* The FLIP scales this whole group from the tile it came from, so the origin
     has to be its middle - the point both boxes are measured around. */
  transform-origin: center center;
  will-change: transform;
}
.weird__viewer-figure img {
  max-width: min(78vw, 620px);
  max-height: 66vh;
  border-radius: 22px;
  image-rendering: auto;
  box-shadow: 0 4px 12px rgba(10, 8, 4, 0.42), 0 32px 80px rgba(10, 8, 4, 0.5);
}
.weird__viewer-figure figcaption {
  font-size: var(--step--1);
  text-align: center;
  letter-spacing: var(--track-body);
  opacity: 0;
  transition: opacity 260ms var(--ease-out) 200ms;
}
.weird__viewer[data-open='true'] figcaption { opacity: 1; }

/* The picture carries the flip, so it needs its own compositing layer and must
   not be squeezed by the grid while it is mid-travel. */
.weird__viewer-figure img { will-change: transform; transform-origin: center center; }

 /* Fixed, not absolute. The viewer is a grid container, and an absolutely
   positioned child of one resolves its offsets against the grid area rather
   than the box you can see - which put "bottom centre" wherever the content
   happened to end. Against the viewport there is nothing to get wrong. */
.weird__viewer-close {
  position: fixed;
  top: var(--sp-5); right: var(--sp-5);
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, white 16%, transparent);
  background: color-mix(in srgb, var(--surface) 62%, transparent);
  backdrop-filter: blur(18px) saturate(160%);
  color: var(--text);
  cursor: pointer;
}

/* One floating group of actions, on the same material as the close button so
   the two read as the same layer of chrome rather than two separate widgets. */
.weird__viewer-actions {
  position: fixed;
  left: 50%;
  bottom: var(--sp-6);
  transform: translateX(-50%);
  display: flex;
  gap: var(--sp-2);
  padding: var(--sp-2);
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, white 14%, transparent);
  background: color-mix(in srgb, var(--surface) 58%, transparent);
  backdrop-filter: blur(20px) saturate(160%);
  box-shadow: 0 12px 40px rgba(10, 8, 4, 0.46);
}
.weird__viewer-actions .btn { border: 0; border-radius: 999px; }

@media (prefers-reduced-motion: reduce) {
  .weird__caret { animation: none; }
  .weird__tile[data-landed] { animation: none; }
  .weird__tile-shimmer { animation: none; }
  .weird__tile::after { animation: none; }
  .weird__tile-img { transition: none; opacity: 1; transform: none; }
}

/* ------------------------------------------------ one surface language -- */
/*
 * G-Images and G-Doodle get what G-Weird got, so the three read as one product
 * rather than three screens that happen to share a sidebar: a softer corner, a
 * hairline of light along the top edge where a real surface would catch it, a
 * shadow with two terms (a tight contact shadow plus a wide ambient one), and a
 * lift toward the pointer that takes the press.
 *
 * Nothing here is decoration for its own sake - depth is what says "this is a
 * thing you can pick up", and the press is what proves the screen heard you
 * before it has done anything.
 */
.preset, .doodle__stage, .studio__said, .weird__tile {
  border-radius: 18px;
}

.preset {
  position: relative;
  overflow: hidden;
  box-shadow: var(--shade-1), var(--shade-2);
  transition: transform 200ms cubic-bezier(0.32, 0.72, 0, 1),
              border-color 200ms var(--ease-out),
              box-shadow 200ms var(--ease-out);
}
.preset::before,
.doodle__stage::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg, transparent,
    color-mix(in srgb, white 20%, transparent), transparent);
  pointer-events: none;
  z-index: 1;
}
.preset:hover {
  border-color: var(--accent-line);
  transform: translateY(-4px);
  box-shadow: var(--shade-1-up), var(--shade-2-up);
}
.preset:active { transform: translateY(-1px) scale(0.99); }

.doodle__stage {
  position: relative;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 6%, transparent),
              var(--shade-2);
}

/* Chips in both panels: the same pill, the same press. */
.doodle__chip, .vocab__chip {
  border-radius: 999px;
  box-shadow: var(--shade-1);
}

/* The version picker is chrome floating over content, so it is a material and
   not a panel: content passes under it rather than stopping at it. */
.picker__menu {
  background: color-mix(in srgb, var(--surface) 74%, transparent);
  backdrop-filter: blur(24px) saturate(170%);
  border-color: color-mix(in srgb, white 12%, var(--line-strong));
}

@media (prefers-reduced-motion: reduce) {
  .preset { transition: none; }
  .preset:hover, .preset:active { transform: none; }
  .weird__tile[data-status='done']:hover { transform: none; }
}

/* A tile that has not been flown into yet. It is the ball's destination, not a
   placeholder: showing an empty square waiting to be filled would make the
   flight decorative, when the point is that one object travels and lands. */
.weird__tile[data-incoming] { opacity: 0; transform: scale(0.6); }

/* The picked decoder version. Same treatment as the other pickers on the
   page, so "which one is active" reads the same everywhere. */
.doodle__chip.is-on { border-color: var(--accent); background: var(--accent-soft); }

/* ---------------------------------------------------- g-weird openers -- */
/*
 * What to write, shown rather than explained.
 *
 * The blank canvas is the best-documented failure in generative interfaces:
 * a caret and nothing else asks a question most people cannot answer, and the
 * ones who guess wrong decide the model is broken rather than that they guessed.
 * These are not buttons in the visual sense - no border, no fill, no hover
 * chrome - because the screen's whole premise is that there is no form here.
 * They read as lines someone has already written, one shade below your own.
 */
.weird__openers {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  margin: var(--sp-3) 0 0;
  padding: 0;
  list-style: none;
}

.weird__opener {
  padding: 2px 0;
  border: 0;
  background: none;
  color: var(--text-faint);
  font-family: var(--font-display);
  font-size: clamp(0.95rem, 1.4vw, 1.15rem);
  letter-spacing: var(--track-title);
  cursor: pointer;
  /* The underline grows from the left on hover rather than switching on: the
     line is the thing being offered, so the emphasis travels along it. */
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0 1px;
  transition: color 180ms var(--ease-out), background-size 260ms var(--ease-out);
}
.weird__opener:hover,
.weird__opener:focus-visible { color: var(--text); background-size: 100% 1px; }

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