/* ---------------------------------------------------------------------------
   Neumorphism ("soft UI") design system.

   The whole look rests on one rule: a control is the SAME colour as the surface
   behind it, and only two shadows separate them — a dark one on the side away
   from the light and a light one on the side facing it. The light source is
   fixed at the top-left everywhere on the page; the moment two elements disagree
   about where the light comes from, the illusion collapses into mush.

   Raised = outer shadows. Pressed/inset (inputs, wells, active buttons) = the
   same two shadows moved inside. There is no third state, and no borders.

   Neumorphism's known weakness is contrast: edges are defined by ~5% luminance
   differences, which fails WCAG for anything load-bearing. So the softness is
   spent only on surfaces — text, icons and focus rings stay at full contrast,
   and every control keeps a visible :focus-visible ring.
   --------------------------------------------------------------------------- */

:root {
  color-scheme: light;

  /* A neutral with a hint of blue reads as "surface" rather than "dirty white",
     and leaves room for a genuinely white highlight. */
  --bg: #e7ecf3;
  --dark: #c3cbd9;
  --light: #ffffff;

  --text: #262c3a;
  --dim: #626d84;
  --faint: #8b95a8;

  --accent: #6c5ce7;
  --accent-soft: #6c5ce726;
  --ok: #129d6e;
  --ok-soft: #129d6e26;
  --warn: #d98324;
  --warn-soft: #d9832426;
  --err: #d94a4a;
  --err-soft: #d94a4a26;

  --r-lg: 26px;
  --r-md: 18px;
  --r-sm: 12px;
  --r-pill: 999px;

  --sh-out: 7px 7px 15px var(--dark), -7px -7px 15px var(--light);
  --sh-out-sm: 4px 4px 9px var(--dark), -4px -4px 9px var(--light);
  --sh-out-lg: 12px 12px 26px var(--dark), -10px -10px 22px var(--light);
  --sh-in: inset 5px 5px 10px var(--dark), inset -5px -5px 10px var(--light);
  --sh-in-sm: inset 3px 3px 6px var(--dark), inset -3px -3px 6px var(--light);

  --ease: cubic-bezier(.34, 1.4, .64, 1);
  --t: .22s var(--ease);

  --maxw: 940px;
}

:root[data-theme="dark"] {
  color-scheme: dark;

  /* Dark neumorphism must sit on a mid grey. On near-black there is no room for
     a darker shadow, so elements flatten out completely. */
  --bg: #272b33;
  --dark: #1b1e24;
  --light: #333944;

  --text: #eaeef6;
  --dim: #a2acc0;
  --faint: #7c869a;

  --accent: #8b7cff;
  --accent-soft: #8b7cff2e;
  --ok: #35c88f;
  --ok-soft: #35c88f2e;
  --warn: #e8a34a;
  --warn-soft: #e8a34a2e;
  --err: #f0706e;
  --err-soft: #f0706e2e;
}

/* Honour the OS until the user picks a side; theme-init.js stamps data-theme. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg: #272b33;
    --dark: #1b1e24;
    --light: #333944;
    --text: #eaeef6;
    --dim: #a2acc0;
    --faint: #7c869a;
    --accent: #8b7cff;
    --accent-soft: #8b7cff2e;
    --ok: #35c88f;
    --ok-soft: #35c88f2e;
    --warn: #e8a34a;
    --warn-soft: #e8a34a2e;
    --err: #f0706e;
    --err-soft: #f0706e2e;
  }
}

/* --------------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--text);
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;

  /* A word with no break opportunity in it — an uploaded file's name, a URL,
     an API key — is laid out at its full width whatever the viewport is, and
     runs straight past the right edge. This only ever acts on a word that
     cannot fit a line of its own, so nothing at a comfortable width changes. */
  overflow-wrap: break-word;

  transition: background-color .3s ease, color .3s ease;
}

/* --- the one-column-grid trap ----------------------------------------------

   Every stack on this page is `display: grid` with a gap and no declared
   columns, which gives it one implicit `auto` track. An `auto` track is
   `minmax(min-content, max-content)`, so its floor is the widest thing inside
   it that cannot be broken — a long file name, a nowrap format row. When that
   floor is wider than the phone, the track wins: the grid keeps its own width,
   the track overflows it, and the content runs off the right of the screen.

   `minmax(0, 1fr)` is the same single column with the floor removed, so the
   track can never be wider than the grid. Children that must not be squeezed
   say so themselves (`.card-head .name` ellipsises), which is the behaviour
   you want on a narrow screen. Where the content already fits, nothing
   changes. */
.cards,
.card,
.field,
.prose,
.prose ul,
.prose .panel,
.footer { grid-template-columns: minmax(0, 1fr); }

h1, h2, h3 { margin: 0; line-height: 1.2; letter-spacing: -.02em; font-weight: 700; }
p { margin: 0; }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

[hidden] { display: none !important; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* --- icons ----------------------------------------------------------------- */

/* Every icon is a <use> of the sprite at the top of the body. Sizing in `em`
   means each one inherits the font-size that was already tuned for the glyph it
   replaced, so one rule covers the 1.8rem dropzone mark and a 0.85rem note. */
.ico {
  width: 1.15em; height: 1.15em; flex: none;
  fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.sprite { display: none; }

/* The toggle offers the theme you are NOT in, so the sun shows while dark. */
#theme-toggle .ico { display: none; }
#theme-toggle[data-mode="dark"] .i-sun,
#theme-toggle[data-mode="light"] .i-moon { display: block; }

/* --- layout ---------------------------------------------------------------- */

.shell {
  width: min(var(--maxw), 100% - 2.5rem);
  margin-inline: auto;
  padding-block: clamp(1.5rem, 4vw, 2.75rem) 4rem;
}

.topbar {
  display: flex; align-items: center; gap: 1rem;
  margin-bottom: clamp(1.75rem, 4vw, 2.5rem);
}

.brand { display: flex; align-items: center; gap: .85rem; margin-right: auto; min-width: 0; }

.brand .mark {
  width: 46px; height: 46px; flex: none;
  border-radius: var(--r-md);
  display: grid; place-items: center;
  font-size: 1.35rem;
  background: var(--bg);
  box-shadow: var(--sh-out-sm);
}

/* The converter's brand is the page's h1; on the wordy pages the h1 belongs to
   the prose, so the wordmark is a plain link there and shares the type. */
.brand h1, .brand .name { font-size: clamp(1.25rem, 3.2vw, 1.5rem); font-weight: 700; line-height: 1.2; }
a.brand, a.brand:hover { color: inherit; text-decoration: none; }
.brand .tag { color: var(--dim); font-size: .82rem; line-height: 1.3; }

/* --- raised surfaces ------------------------------------------------------- */

.panel {
  background: var(--bg);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-out);
  padding: clamp(1.15rem, 3vw, 1.75rem);
}

.panel + .panel { margin-top: 1.5rem; }

.well {
  background: var(--bg);
  border-radius: var(--r-md);
  box-shadow: var(--sh-in);
  padding: 1rem 1.15rem;
}

/* --- buttons --------------------------------------------------------------- */

.btn {
  appearance: none; border: 0; cursor: pointer;
  font: inherit; font-weight: 650; color: var(--text);
  background: var(--bg);
  border-radius: var(--r-md);
  box-shadow: var(--sh-out-sm);
  padding: .7rem 1.25rem;
  display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
  transition: box-shadow var(--t), transform var(--t), color var(--t);
}
.btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: var(--sh-out); }
/* Pressing pushes the control *into* the surface — the inset is the whole point. */
.btn:active:not(:disabled) { transform: translateY(0); box-shadow: var(--sh-in-sm); }

.btn:disabled { cursor: not-allowed; opacity: .5; box-shadow: var(--sh-in-sm); transform: none; }

.btn-primary { color: var(--accent); }
.btn-primary .label { font-weight: 700; }

.btn-icon {
  width: 46px; height: 46px; padding: 0; flex: none;
  border-radius: var(--r-md); font-size: 1.1rem;
}

.btn-sm { padding: .45rem .85rem; font-size: .85rem; border-radius: var(--r-sm); }

.btn-ghost { box-shadow: none; color: var(--dim); }
.btn-ghost:hover:not(:disabled) { box-shadow: var(--sh-out-sm); color: var(--text); transform: none; }

/* --- segmented control (tabs) ---------------------------------------------- */

.segmented {
  position: relative;
  display: flex; gap: .4rem; padding: .4rem;
  background: var(--bg); border-radius: var(--r-md); box-shadow: var(--sh-in);
  margin-bottom: 1.5rem;
}

/* The raised state is a single sliding tile rather than a shadow that pops on
   and off each button — a neumorphic control is a physical thing, so the lit
   surface should travel to the tab you picked instead of teleporting. JS
   measures the active button and drives width + translateX. */
.segmented .thumb {
  position: absolute;
  top: .4rem; left: 0;
  height: calc(100% - .8rem);
  width: 0;
  border-radius: var(--r-sm);
  background: var(--bg);
  box-shadow: var(--sh-out-sm);
  transition: transform .38s var(--ease), width .38s var(--ease), opacity .22s ease;
  pointer-events: none;
}
/* Set until the first measurement lands, so the tile doesn't fly in from the
   left edge on load. */
.segmented .thumb.no-anim { transition: none; }

.segmented button {
  position: relative; z-index: 1;
  flex: 1; appearance: none; border: 0; cursor: pointer;
  font: inherit; font-weight: 650; color: var(--dim);
  background: transparent; border-radius: var(--r-sm);
  padding: .6rem .5rem;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color var(--t);
}
.segmented button[aria-selected="true"] { color: var(--accent); }
.segmented button:not([aria-selected="true"]):hover { color: var(--text); }

/* --- form controls --------------------------------------------------------- */

.field { display: grid; gap: .4rem; }
.field > .lbl { font-size: .78rem; font-weight: 650; color: var(--dim); letter-spacing: .02em; }

.input, .select {
  appearance: none; border: 0; width: 100%;
  font: inherit; color: var(--text);
  background: var(--bg);
  border-radius: var(--r-md);
  box-shadow: var(--sh-in-sm);
  padding: .75rem 1rem;
  transition: box-shadow var(--t);
}
.input::placeholder { color: var(--faint); }
.input:focus, .select:focus { box-shadow: var(--sh-in); outline-offset: 4px; }

.select {
  padding-right: 2.6rem;
  /* Inline SVG caret keeps the CSP clean — no external asset, no data fetch. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%23808aa0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 1.1rem center;
}
.select optgroup { font-weight: 700; color: var(--dim); }
.select option { color: var(--text); background: var(--bg); }

input[type="range"] {
  appearance: none; width: 100%; height: 22px; background: transparent; cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
  height: 10px; border-radius: var(--r-pill);
  background: var(--bg); box-shadow: var(--sh-in-sm);
}
input[type="range"]::-moz-range-track {
  height: 10px; border-radius: var(--r-pill);
  background: var(--bg); box-shadow: var(--sh-in-sm);
}
input[type="range"]::-webkit-slider-thumb {
  appearance: none; width: 20px; height: 20px; margin-top: -5px;
  border-radius: 50%; background: var(--bg); box-shadow: var(--sh-out-sm);
}
input[type="range"]::-moz-range-thumb {
  width: 20px; height: 20px; border: 0;
  border-radius: 50%; background: var(--bg); box-shadow: var(--sh-out-sm);
}

input[type="color"] {
  appearance: none; border: 0; background: transparent;
  width: 46px; height: 40px; padding: 0; cursor: pointer;
  border-radius: var(--r-sm); box-shadow: var(--sh-out-sm);
}
input[type="color"]::-webkit-color-swatch-wrapper { padding: 5px; }
input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 7px; }

/* --- dropzone -------------------------------------------------------------- */

.dropzone {
  position: relative;
  display: grid; place-items: center; gap: .65rem;
  text-align: center;
  padding: clamp(2rem, 7vw, 3.5rem) 1.5rem;
  border-radius: var(--r-lg);
  background: var(--bg);
  box-shadow: var(--sh-in);
  cursor: pointer;
  transition: box-shadow var(--t), transform var(--t);
}
.dropzone:hover { box-shadow: var(--sh-in-sm); }
.dropzone.is-over {
  box-shadow: var(--sh-in), 0 0 0 3px var(--accent) inset;
  transform: scale(.995);
}
.dropzone .glyph {
  width: 68px; height: 68px; border-radius: 22px;
  display: grid; place-items: center; font-size: 1.8rem;
  background: var(--bg); box-shadow: var(--sh-out-sm);
}
.dropzone .hint { color: var(--dim); font-size: .9rem; max-width: 34ch; }
.dropzone input[type="file"] { display: none; }

/* --- file cards ------------------------------------------------------------ */

.cards { display: grid; gap: 1.1rem; margin-top: 1.5rem; }

.card {
  background: var(--bg);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-out);
  padding: 1.15rem;
  display: grid; gap: .9rem;
}

.card-head { display: flex; align-items: flex-start; gap: .85rem; }
.card-head .glyph {
  width: 44px; height: 44px; flex: none; border-radius: var(--r-sm);
  display: grid; place-items: center; font-size: 1.2rem;
  background: var(--bg); box-shadow: var(--sh-in-sm);
}
.card-head .meta { min-width: 0; flex: 1; }
.card-head .name {
  font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.card-head .sub { font-size: .8rem; color: var(--dim); }

.card-controls {
  display: grid; gap: .8rem;
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 620px) {
  .card-controls { grid-template-columns: minmax(0, 1fr) auto; align-items: end; }
}

.options {
  display: grid; gap: .85rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
  padding-top: .25rem;
}

/* --- progress -------------------------------------------------------------- */

.bar {
  height: 12px; border-radius: var(--r-pill);
  background: var(--bg); box-shadow: var(--sh-in-sm);
  overflow: hidden;
}
.bar > i {
  display: block; height: 100%; width: 0%;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 55%, var(--ok)));
  transition: width .45s ease;
}
.bar.indeterminate > i {
  width: 35%;
  animation: slide 1.3s var(--ease) infinite;
}
@keyframes slide {
  0% { transform: translateX(-110%); }
  100% { transform: translateX(320%); }
}

/* --- status chips ---------------------------------------------------------- */

.chip {
  display: inline-flex; align-items: center; gap: .4rem;
  font-size: .74rem; font-weight: 700; letter-spacing: .03em; text-transform: uppercase;
  padding: .3em .8em; border-radius: var(--r-pill);
  color: var(--dim); background: var(--accent-soft);
}
.chip.queued  { color: var(--dim);  background: color-mix(in srgb, var(--dim) 14%, transparent); }
.chip.running { color: var(--accent); background: var(--accent-soft); }
.chip.done    { color: var(--ok);   background: var(--ok-soft); }
.chip.error   { color: var(--err);  background: var(--err-soft); }
.chip.expired { color: var(--faint); background: color-mix(in srgb, var(--faint) 14%, transparent); }

.note {
  font-size: .85rem; padding: .7rem .95rem; border-radius: var(--r-sm);
  display: flex; gap: .55rem; align-items: flex-start;
}
.note.warn { color: var(--warn); background: var(--warn-soft); }
.note.err  { color: var(--err);  background: var(--err-soft); }
.note.ok   { color: var(--ok);   background: var(--ok-soft); }

/* --- long-form pages (privacy, cookies) ------------------------------------ */

/* The converter is one screen of controls; these two are the only pages on the
   site that are just words, so they get the one thing the rest does not need —
   a measure. Everything else is the same surface language. */
.prose { display: grid; gap: 1.15rem; max-width: 68ch; }
.prose h1 { font-size: clamp(1.45rem, 5vw, 1.95rem); }
.prose h2 { font-size: 1.1rem; margin-top: .9rem; }
.prose p, .prose li { color: var(--dim); }
.prose .lede { font-size: 1.02rem; }
.prose strong { color: var(--text); font-weight: 650; }
.prose ul { margin: 0; padding-left: 1.2rem; display: grid; gap: .55rem; }
.prose li::marker { color: var(--faint); }
.prose code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .88em; padding: .1em .4em; border-radius: 6px;
  background: var(--bg); box-shadow: var(--sh-in-sm); color: var(--text);
}
.prose .panel { display: grid; gap: .6rem; }
.prose .panel h2 { margin-top: 0; }
.prose .updated { color: var(--faint); font-size: .82rem; }

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

.footer {
  margin-top: 3rem; text-align: center; color: var(--dim); font-size: .84rem;
  display: grid; gap: .75rem;
}
.footer .pills { display: flex; flex-wrap: wrap; gap: .5rem; justify-content: center; }
.footer .pills span {
  padding: .35em .9em; border-radius: var(--r-pill);
  background: var(--bg); box-shadow: var(--sh-out-sm); font-size: .78rem; font-weight: 600;
}

/* --- motion ---------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* Neumorphism has no borders to fall back on, so a forced-colors user would see
   a blank page. Reinstate real outlines there. */
@media (forced-colors: active) {
  .panel, .card, .well, .btn, .input, .select, .dropzone, .footer .pills span,
  .segmented {
    border: 1px solid CanvasText;
    box-shadow: none;
  }
  /* The sliding tile is pure shadow, so it has nothing left to show here — the
     selected tab is already identified by aria-selected and its accent colour. */
  .segmented .thumb { display: none; }
}

/* --- skeleton placeholders -------------------------------------------------
   A placeholder here is a HOLLOW, not a grey box.

   The usual skeleton — a flat light-grey rectangle with a white bar sliding
   across it — has no light source, and on a page where every single surface is
   shaped by one fixed top-left light it reads as a foreign object dropped onto
   the design. So a placeholder is instead the one thing this system already
   says for "nothing here yet": a dent pressed into the surface, with the same
   inset shadow pair as an empty input or a meter track. A soft highlight then
   travels across the dent on the page's own diagonal.

   The sweep animates a transform on a pseudo-element rather than
   background-position: a transform can be handed to the compositor, so a grid
   of twenty placeholders costs no layout and no repaint while it runs.
   ------------------------------------------------------------------------- */

/* How bright the travelling highlight is.

   It has to be set per theme, and that is not a fudge: --light is a genuine
   white on the light theme but only a slightly-lighter grey on the dark one,
   so one number cannot serve both. At full strength on the light theme the
   band washes the dent out completely; at 68% on the dark theme it is
   invisible. Each side gets the strongest sheen that still leaves the hollow
   readable underneath — and neither invents a colour the palette does not
   already contain. */
:root { --sk-sheen: color-mix(in srgb, var(--light) 68%, transparent); }
:root[data-theme="dark"] { --sk-sheen: var(--light); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --sk-sheen: var(--light); }
}

.sk {
  position: relative;
  overflow: hidden;
  background: var(--bg);
  border-radius: var(--r-sm);
  box-shadow: var(--sh-in-sm);
  /* Placeholders are furniture. The container carries aria-busy; these must
     never be clickable, selectable, or read out as content. */
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
}

.sk::after {
  content: "";
  position: absolute;
  inset: 0;
  /* 100deg rather than 90: the highlight travels along the same diagonal the
     rest of the page is lit from. */
  background: linear-gradient(100deg,
    transparent 22%,
    var(--sk-sheen) 50%,
    transparent 78%);
  transform: translateX(-100%);
  animation: sk-sweep 1.7s cubic-bezier(.4, 0, .25, 1) infinite;
}

@keyframes sk-sweep { to { transform: translateX(100%); } }

/* Shapes. Every one of these stands in for something real, so it carries that
   thing's height — a placeholder that is the wrong size moves the page when the
   content lands, which is the whole problem placeholders exist to solve.

   None of them carries a margin: every container that holds a stack of them —
   a .card, a .tile, a .panel — is already a grid or a flex box with its own
   gap, so a margin here would only double it. */
.sk-line   { height: .72rem; border-radius: var(--r-pill); }
.sk-line.short { width: 52%; }
.sk-line.mid   { width: 76%; }
.sk-title  { height: 1.05rem; width: 62%; border-radius: var(--r-pill); }
.sk-chip   { height: 1.5rem; width: 4.6rem; border-radius: var(--r-pill); }
.sk-btn    { height: 2.2rem; width: 6rem; border-radius: var(--r-sm); }
.sk-num    { height: 2rem;   width: 4.5rem; }
.sk-dot    { height: .72rem; width: .72rem; border-radius: 50%; flex: none; }
.sk-block  { height: 100%; width: 100%; border-radius: var(--r-md); }

/* A skeleton card wears the same shell as the card it stands in for, so the
   handover moves nothing. */
.skeleton { pointer-events: none; }

/* Reduced motion keeps the dent — which is the part that actually says
   "loading" — and stops the sweep travelling. */
@media (prefers-reduced-motion: reduce) {
  .sk::after { animation: none; transform: none; opacity: .55; }
}

/* --- arrival ---------------------------------------------------------------
   Two ways content shows up, one motion: a short rise out of the surface.

   `.entering` is for content that replaces a placeholder — it plays once, the
   moment the data lands. `[data-reveal]` is for content that is already in the
   document and waits until you scroll to it.

   The hidden half of the reveal is gated behind `html.reveal`, a class only
   the head script sets and only when it has checked that IntersectionObserver
   exists and the visitor has not asked for reduced motion. Nothing in this
   file can hide content on its own: with no JavaScript, an old browser, or a
   module that failed to download, every [data-reveal] element is simply
   visible. A scroll flourish must never be the reason a page cannot be read.
   ------------------------------------------------------------------------- */

@keyframes sk-enter {
  from { opacity: 0; transform: translateY(10px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

/* `both` holds the from-state through the stagger delay; the JS strips the
   class on animationend, because a lingering fill-mode transform turns the
   element into a containing block and would quietly break any position:fixed
   or sticky descendant. */
.entering { animation: sk-enter .45s cubic-bezier(.22, .8, .3, 1) both; }

html.reveal [data-reveal] {
  opacity: 0;
  transform: translateY(16px);
}

html.reveal [data-reveal].revealed {
  opacity: 1;
  transform: none;
  transition: opacity .55s cubic-bezier(.22, .8, .3, 1),
              transform .55s cubic-bezier(.22, .8, .3, 1);
}

@media (prefers-reduced-motion: reduce) {
  .entering { animation: none; }
  html.reveal [data-reveal] { opacity: 1; transform: none; }
}

/* The placeholder shells the converter uses. Each keeps the height of the real
   thing, so the page does not jump when the answer lands. */
.panel.skeleton { display: grid; gap: .75rem; }
.skeleton .sk-row { display: flex; align-items: center; gap: .6rem; }
.skeleton .sk-row .sk-line { flex: 1; }

/* --- arrival, part two: the first load -------------------------------------
   The reveal above starts wherever the first marked element is, which until
   now was below the header: the top bar, the tabs and the whole first panel
   were simply there, fully formed, so the first thing a visitor saw was
   static. Marking the chrome as well lets the same observer cascade the page
   from the top down with no new machinery.

   That needs two more variants of the one motion, because "fade and rise" is
   wrong for two kinds of element.
   ------------------------------------------------------------------------- */

/* The neutral the sheet arrival starts from: the same two shadows at zero
   offset and zero blur, so they sit entirely behind the box and the surface
   reads as unbroken. It has to be a real shadow pair rather than `none`,
   because box-shadow only interpolates between lists of the same length —
   `none` snaps. */
:root { --sh-flat: 0 0 0 var(--dark), 0 0 0 var(--light); }

/* "pop" is for a raised control — a button, a card, a stat tile. It starts
   fractionally smaller, and a transform scales an element's box-shadow along
   with it, so the shadow grows as the thing rises. In this design system that
   IS "raised", and it costs no shadow animation to get. */
html.reveal [data-reveal="pop"] {
  transform: translateY(14px) scale(.97);
}

/* "sheet" is for a panel that has marked children of its own. It must not fade
   or move: the same pixels would then animate twice, once with the panel and
   once on their own. Its arrival is its own shadow spreading out of the flat
   surface instead, while the content cascades inside it.

   --sh-sheet is the escape hatch for a panel whose resting shadow is not
   --sh-out. Getting that wrong is visible: retire() drops the attribute when
   the transition ends, and the element would jump to its real shadow. */
html.reveal [data-reveal="sheet"] {
  opacity: 1;
  transform: none;
  box-shadow: var(--sh-flat);
}
html.reveal [data-reveal="sheet"].revealed {
  box-shadow: var(--sh-sheet, var(--sh-out));
  transition: box-shadow .7s cubic-bezier(.22, .8, .3, 1);
}

/* The one raised thing the cascade would otherwise skip. The sliding tile
   cannot carry its own [data-reveal] — script owns its transform and width,
   and a transition on those is how it moves between tabs — so it borrows the
   bar's arrival time through --arrive-delay and animates the only property
   nothing else is using. Both rules stop matching when the bar retires, which
   lands the tile on exactly the --sh-out-sm it started the page with. */
@keyframes thumbRise {
  from { box-shadow: var(--sh-flat); }
  to   { box-shadow: var(--sh-out-sm); }
}
html.reveal .segmented[data-reveal] .thumb { box-shadow: var(--sh-flat); }
html.reveal .segmented[data-reveal].revealed .thumb {
  animation: thumbRise .55s cubic-bezier(.22, .8, .3, 1) var(--arrive-delay, 0ms) both;
}

@media (prefers-reduced-motion: reduce) {
  html.reveal [data-reveal="sheet"] { box-shadow: var(--sh-sheet, var(--sh-out)); }
  html.reveal .segmented[data-reveal] .thumb { box-shadow: var(--sh-out-sm); }
  html.reveal .segmented[data-reveal].revealed .thumb { animation: none; }
}
