/* The title above the mirror is gone, and Alte Haas Grotesk went with it — it
   was the only thing that ever set the face, so the @font-face has come out
   rather than sit here fetching a 200KB TTF for nothing. `assets/fonts/` is
   unreferenced now; it is still on disk in case the title comes back. */

:root {
  --ink: #f2e6e2;
  --muted: #a99691;
  /* The glass and the frame share an edge now, so they share a radius. */
  --glass-radius: 20px;
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }

html {
  background: #0b0708;   /* under the photograph, and before it loads */
}

/* Deliberately no background here. The room below sits in its own layer, and
   an opaque background on <body> paints over anything behind it — body's
   background comes after negative and zero z-index descendants in the paint
   order, so the photograph disappears without a trace. */
html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  color: var(--ink);
  font-family: "Helvetica Neue", Inter, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* The room the booth stands in: a red-tiled washroom, graffitied.
   Its own element rather than a background on <body> so the scrim can be a
   child — a background-image and a gradient on one element can't be filtered
   separately, and the photo wants the blur while the scrim must not have it. */
.room {
  position: fixed;
  inset: 0;
  z-index: 0;
  background: url("assets/washroom.jpg") center / cover no-repeat;
  /* Slightly out of focus: it is the room behind you, and in focus its graffiti
     competes with the marks on the glass — same red, same handwriting, and the
     marks are the subject. Brightened rather than dimmed, because the source
     photograph is dark to begin with; its mean luminance is 61 of 255, so the
     dark scrim this started with took it to almost nothing.
     Scaled up because blur eats into the edges of its own element. */
  filter: blur(2px) saturate(1.05) brightness(1.18);
  transform: scale(1.06);
}

/* Heaviest top and bottom, so the controls stay legible over whatever part of
   the photograph lands behind them. The top is still weighted even though the
   title that used to sit up there is gone: it is what stops the glass reading
   as the top edge of the picture rather than an object standing in a room. */
.room::after {
  content: "";
  position: fixed;
  inset: 0;
  background:
    linear-gradient(180deg, rgba(8, 5, 6, 0.78) 0%, rgba(8, 5, 6, 0.06) 22%,
                    rgba(8, 5, 6, 0.10) 70%, rgba(8, 5, 6, 0.82) 100%),
    radial-gradient(88% 66% at 50% 46%, transparent 0%, rgba(8, 5, 6, 0.3) 100%);
}

/* ---------- full-bleed mirror ---------- */

.booth {
  position: fixed;
  inset: 0;
  z-index: 1;            /* above the room */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(10px, 1.8vh, 20px);
  /* More at the top than the title used to leave, not less. The mirror's
     backlight is a shadow spread on `.frame` and it needs somewhere to fall;
     with the title gone the glass rose until the glow was being clipped by the
     top of the window, which reads as the picture ending rather than as a lit
     object with room above it. */
  padding:
    clamp(20px, 4.5vh, 48px)
    clamp(12px, 3vmin, 36px)
    /* Plus the home indicator, on the phones that have one. The page is
       `viewport-fit=cover`, so the bottom of the window is under it rather
       than above it, and the last line in this column used to be the status
       text — losing a little of that to the indicator cost legibility and
       nothing else. The last line is a link now, and the strip along the
       bottom of an iPhone belongs to the system gesture: a control sitting
       in it is not merely dim, it cannot be pressed. Resolves to `0px` on
       everything without an inset, so nothing else moves. */
    calc(clamp(14px, 2.6vh, 30px) + env(safe-area-inset-bottom, 0px));
}

/* Takes whatever height the title and controls leave, so the glass stays as
   big as it can be while still reading as a framed object rather than the
   whole screen. */
.mirror {
  flex: 1;
  min-height: 0;
  width: 100%;
  display: flex;
  justify-content: center;
}

/*
 * A backlit mirror with no border at all: the glass runs to its own rounded
 * edge, and a light behind it throws a halo onto the wall.
 *
 * There was a dark rim here and it read as a hard black outline drawn around
 * the picture rather than an object with an edge. What's left is a hairline —
 * light, not dark, and barely there — which is enough to stop the glass
 * dissolving into the glow without ever becoming a line you look at.
 *
 * The halo is box-shadow rather than a pseudo-element because box-shadow paints
 * outside the border box on its own, with no stacking order to arrange — and
 * the room behind is a sibling layer, so anything using z-index here has to be
 * reasoned about against that.
 *
 * Three spreads rather than one: a tight bright edge where the light escapes
 * closest to the glass, a wide soft one for the fall across the wall, and a
 * plain black drop so the mirror still sits off the wall rather than glowing
 * while floating flat against it.
 */
.frame {
  position: relative;
  height: 100%;
  max-width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: var(--glass-radius);
  box-shadow:
    0 0 26px 4px rgba(255, 122, 74, 0.42),
    0 0 90px 26px rgba(240, 66, 34, 0.3),
    0 0 190px 70px rgba(214, 44, 26, 0.18),
    0 26px 64px rgba(0, 0, 0, 0.62);
}

/* The hairline, over the glass so it isn't clipped by its overflow. */
.frame::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 4;
  box-shadow: inset 0 0 0 1px rgba(255, 226, 218, 0.13);
}

.glass {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  /* The frame's own radius: with the rim gone they are the same edge. */
  border-radius: var(--glass-radius);
  background: #050405;
}

#video {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

#stage {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
  cursor: crosshair;
}


/* ---------- overlays ---------- */

/* ---------- how to ---------- */

.sheet {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 5vmin;
  background: rgba(6, 4, 5, 0.82);
  backdrop-filter: blur(14px);
}

.sheet-panel {
  width: min(760px, 100%);
  max-height: 86vh;
  overflow-y: auto;
  padding: clamp(22px, 3.4vmin, 34px);
  border-radius: 16px;
  background: rgba(24, 15, 17, 0.96);
  border: 1px solid rgba(255, 190, 175, 0.16);
  box-shadow: 0 0 60px 8px rgba(240, 80, 48, 0.16), 0 30px 70px rgba(0, 0, 0, 0.7);
}

.sheet-panel h2 {
  margin: 0 0 18px;
  font-family: "Didot", "Bodoni 72", "Playfair Display", Georgia, serif;
  font-weight: 400;
  font-size: clamp(20px, 2.8vmin, 28px);
  color: #f6e9e6;
}

/* A column width rather than a count, so the second column is dropped by the
   browser when the panel is too narrow to hold one — on a phone this is a
   plain list again with nothing to configure. */
.how-list {
  margin: 0;
  columns: 250px 2;
  column-gap: 32px;
}

.how-item {
  -webkit-column-break-inside: avoid;
  break-inside: avoid;
  margin-bottom: 15px;
}

/* The gap belongs to the item, not to the space between two of them: a
   margin-top would be collapsed away at the top of a column and push the
   second column down out of line with the first. */
.how-item:last-child { margin-bottom: 0; }

.how-item.full {
  -webkit-column-span: all;
  column-span: all;
  margin-top: 3px;
}

.how-list dt {
  font-size: clamp(11px, 1.4vmin, 13px);
  color: #f0c9c2;
}

.how-list dd {
  margin: 4px 0 0;
  font-size: clamp(11px, 1.4vmin, 13px);
  line-height: 1.5;
  color: var(--muted);
}

.sheet-actions {
  margin-top: 24px;
  display: flex;
  justify-content: flex-end;
}

/* Outside the frame now: on the booth wall rather than over the glass, which
   keeps the whole reflection clear to draw on. */
.controls {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(7px, 1.2vh, 12px);
}

/* ---------- tools ---------- */

/*
 * The shutter dead centre, flanked by two groups.
 *
 * Three columns and not a flex row, because the flanks are uneven — two pills
 * on the left, one on the right — and a centred flex row centres the *row*,
 * which leaves the shutter sitting off to one side of the screen. Both `1fr`
 * tracks resolve to the same width in an auto-sized grid, so the middle column
 * lands in the centre however many pills either side ends up holding.
 *
 * This replaced a single row of six: undo, clear, shutter, the four looks and
 * how-to, all the same size and all the same distance apart, which gave no clue
 * about what belonged with what. Nothing was added to fix it — the same
 * controls, in three groups with one job each.
 */
.actions {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;   /* the shutter is taller than the pills either side */
  gap: clamp(10px, 2.2vmin, 20px);
}

.actions .side {
  display: flex;
  gap: 9px;
  flex-wrap: wrap;
}
/* Both groups crowd toward the shutter rather than the screen edges, so the
   distance from it reads the same on each side. */
.actions .side:first-child { justify-content: flex-end; }
.actions .side:last-child { justify-content: flex-start; }

/*
 * The shutter, in the middle with the rest flanking it — a phone camera's
 * arrangement, because that is what everyone already knows how to use.
 *
 * A ring with a disc inside it rather than one filled circle: the gap between
 * them is most of what makes the shape read as a shutter, and it gives the
 * press somewhere to go, since the disc shrinks inside a ring that stays put.
 */
.shutter {
  width: clamp(52px, 7.4vmin, 68px);
  aspect-ratio: 1;
  padding: 0;
  margin: 0 5px;
  border-radius: 50%;
  border: 2px solid rgba(255, 236, 232, 0.85);
  background: transparent;
  display: grid;
  place-items: center;
  cursor: pointer;
}

.shutter::after {
  content: "";
  width: calc(100% - 11px);
  height: calc(100% - 11px);
  border-radius: 50%;
  background: linear-gradient(180deg, #e8465a, #ab1226);
  box-shadow: 0 0 18px rgba(232, 70, 90, 0.45);
  transition: transform 120ms ease, filter 120ms ease;
}

.shutter:hover::after { filter: brightness(1.12); }
.shutter:active::after { transform: scale(0.88); }

.shutter:disabled { cursor: default; }
.shutter:disabled::after {
  background: rgba(255, 255, 255, 0.22);
  box-shadow: none;
}

.tool {
  font: inherit;
  background: rgba(255, 255, 255, 0.05);
  font-size: clamp(10px, 1.4vmin, 12px);
  letter-spacing: 0.02em;
  color: var(--ink);
  text-decoration: none;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 999px;
  padding: clamp(8px, 1.3vmin, 12px) clamp(14px, 2.2vmin, 22px);
  cursor: pointer;
  transition: background 160ms ease, border-color 160ms ease;
}
.tool:hover { background: rgba(255, 255, 255, 0.16); }
.tool:disabled {
  opacity: 0.4;
  cursor: default;
  filter: none;
}
.tool:disabled:hover { background: rgba(255, 255, 255, 0.05); }
.tool[aria-pressed="true"],
/* Lit while hand tracking is *off*, which is backwards from every other lit
   control here — deliberately. This is the one setting whose effect is that
   nothing happens, so the state that needs announcing is the switched-off one. */
.tool.off {
  background: rgba(255, 90, 110, 0.3);
  border-color: rgba(255, 120, 140, 0.7);
}

/* ---------- how the mirror looks ---------- */

/*
 * Four settings, one choice, so they read as one control: a single pill divided
 * into segments rather than four pills that happen to sit together. Loose ones
 * would say "four things you can switch on", and there is only ever one on.
 *
 * The group keeps the border and the radius; the segments give up both and
 * carry a hairline between them instead, so the outline stays a single
 * unbroken shape.
 */
.looks {
  display: flex;
  align-items: stretch;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 999px;
  overflow: hidden;   /* the ends of the pill clip the segments to the curve */
}

.look {
  font: inherit;
  font-size: clamp(10px, 1.4vmin, 12px);
  letter-spacing: 0.02em;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-left: 1px solid rgba(255, 255, 255, 0.14);
  padding: clamp(8px, 1.3vmin, 12px) clamp(10px, 1.7vmin, 17px);
  cursor: pointer;
  white-space: nowrap;
  transition: background 160ms ease;
}
.look:first-child { border-left: 0; }
.look:hover { background: rgba(255, 255, 255, 0.12); }

/*
 * Each lights up in roughly the colour it turns the glass, which is the only
 * preview of a look that fits on a button. They are not all at the same alpha:
 * a cold blue over a dark red room reads quieter than a pink does, and segments
 * of one control have to look equally on.
 *
 * The phone's is the awkward one, because what it does to the glass is mostly
 * *pale* and `normal` has already taken white. Blue enough to be a colour rather
 * than a second white, and a shade over the others' alpha so the cold reads at
 * this size — it is the coolest thing on the row and has to look it.
 */
#look-normal[aria-pressed="true"] { background: rgba(255, 255, 255, 0.22); }
#look-red[aria-pressed="true"] { background: rgba(255, 90, 110, 0.3); }
#look-nokia[aria-pressed="true"] { background: rgba(146, 204, 240, 0.34); }
#look-vhs[aria-pressed="true"] { background: rgba(216, 118, 232, 0.32); }
/* The odd one out, and deliberately. Every other lit segment is a wash of
   colour over the same pale label, which is no use to the one setting that has
   no colour — sat next to `normal`'s white it would have read as a second,
   slightly different white. Dark type on a solid ground instead: the only thing
   on the row that is literally black and white. */
#look-bw[aria-pressed="true"] {
  background: rgba(255, 255, 255, 0.88);
  color: #16090d;
}
.tool.primary {
  background: linear-gradient(180deg, #d8283c, #9c0f22);
  border-color: rgba(255, 150, 160, 0.5);
  color: #fff;
}
.tool.primary:hover { filter: brightness(1.12); }

/* ---------- tracking indicator ---------- */

.tracking {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: clamp(9px, 1.2vmin, 11px);
  letter-spacing: 0.02em;
  color: var(--muted);
}

.tracking .dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #6b6b6b;
  box-shadow: 0 0 9px currentColor;
  transition: background 220ms ease, transform 220ms ease;
}

.tracking[data-state="searching"] { color: #b9a08c; }
.tracking[data-state="searching"] .dot { background: #c08a4a; }

.tracking[data-state="bridging"] { color: #d8b45f; }
.tracking[data-state="bridging"] .dot { background: #e0b74f; transform: scale(1.25); }

.tracking[data-state="ready"] { color: #93c4a4; }
.tracking[data-state="ready"] .dot { background: #5fd08a; }

.tracking[data-state="drawing"] { color: #f0a3ae; }
.tracking[data-state="drawing"] .dot { background: #ff5f77; transform: scale(1.35); }

.tracking[data-state="erasing"] { color: #a9c4e0; }
.tracking[data-state="erasing"] .dot { background: #7fb2e8; transform: scale(1.35); }

/* The only unlit state on the row. Every other dot is a colour saying what the
   mirror is doing; this one is off, because it isn't doing anything — and the
   glow goes with it, since a dark dot still haloed reads as a light on. */
.tracking[data-state="paused"] { color: var(--muted); }
.tracking[data-state="paused"] .dot {
  background: #4a3f42;
  box-shadow: none;
  transform: scale(0.85);
}

.status {
  margin: 0;
  font-size: clamp(9px, 1.2vmin, 11px);
  letter-spacing: 0.02em;
  color: var(--muted);
  min-height: 13px;
  text-align: center;
}

/* ---------- the signature and the tip jar ---------- */

/*
 * The bottom line: who made it, then the way to buy them a coffee. One row, so
 * the two halves read as one aside rather than as two things stacked up under
 * the controls each asking for a look.
 *
 * Both halves lead somewhere, and they say so differently, because they are
 * different kinds of link. The tip is a standalone thing to press and takes a
 * hover box, the way the pills above it do. The handle is a word inside a
 * sentence and takes an underline, which is what an inline link has always
 * looked like — a box drawn around two words in the middle of a phrase reads
 * as a button someone dropped into the text.
 *
 * The row wraps, because the two halves are independent and a narrow enough
 * window should drop the tip under the name rather than run it off the side.
 */
.footline {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  /* No gap. The space either side of the separator is padding on the two
     halves instead, and it has to be, because only one of them has a hover box
     to keep its words off the edge of. Set as a gap it lands on top of that
     box's 13px and the dot ends up tight against the name and adrift from the
     cup. */
  gap: 0;
}

/*
 * Set with the tip beside it, in the status line's ink, and padded to exactly
 * the same box. That is what puts the separator the same distance from the
 * name as from the cup, and it leaves the same inset at each end of the row so
 * the line sits centred on the shutter above rather than shunted off it by a
 * hover box only one half has.
 */
.credit {
  font-size: clamp(10px, 1.3vmin, 12px);
  letter-spacing: 0.02em;
  color: var(--muted);
  padding: 9px 13px;
}

/*
 * The handle, which is the only part of the credit that goes anywhere.
 *
 * Underlined on hover rather than always, so the line reads as a sentence at
 * rest and as a link the moment you go near it — and offset off the baseline,
 * because at ten pixels a rule sitting on it collides with the descender of
 * the `y` and the tail stops being legible.
 *
 * The padding is a hit target and nothing else: vertical padding on an inline
 * box is painted and hit-tested without changing the height of the line, so
 * this grows a twelve-pixel-tall word into something around thirty to press
 * without moving anything on the row. The negative margin gives back the
 * sideways part, which would otherwise open a gap around the handle inside its
 * own sentence.
 */
.who {
  color: inherit;
  text-decoration: none;
  padding: 9px 3px;
  margin: 0 -3px;
  transition: color 160ms ease;
}
.who:hover {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.3em;
  text-decoration-color: rgba(242, 230, 226, 0.45);
}

/* The house separator, as used in the keyboard row of the how-to sheet. Dimmer
   than what it divides, so it holds the line together without being read as a
   third item on it. */
.sep {
  font-size: clamp(10px, 1.3vmin, 12px);
  color: var(--muted);
  opacity: 0.55;
}

/*
 * Under the machine rather than part of it, so it is a link and not a pill: a
 * `.tool` here would be a fourth control in a set that was deliberately cut to
 * three groups, and the only one of them that takes you off the page.
 *
 * It is set in the pills' size and the status line's ink, which is the whole
 * of the hierarchy it needs. It started at the status line's size as well, and
 * that was too far: at the bottom of the clamp that is nine pixels, and a tip
 * jar nobody can read is not a tip jar. Colour and shape are already doing the
 * work of keeping it under the controls — muted rather than full ink, and no
 * border at all — so the size was free to come back up to something legible.
 *
 * Hover is what separates it from the status line above it, which is text and
 * stays put. This lifts to full-strength ink with the cup filling in behind
 * the words, so the difference between machine chatter and somewhere you can
 * go costs one movement rather than a click to find out.
 *
 * The padding is not for the look of it. Ten pixels of type is nothing to hit
 * on a phone, and this is the only thing down here that can be tapped at all —
 * the box gives the tap somewhere to land without the line ever drawing a
 * border around itself. It is still short of the 44px a control is supposed to
 * be, and deliberately: at 44 this stops being an aside and becomes a fourth
 * button, which is the one thing it must not look like. 32 is what that
 * tension settles on.
 */
.tip {
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  font-size: clamp(10px, 1.3vmin, 12px);
  letter-spacing: 0.02em;
  color: var(--muted);
  text-decoration: none;
  padding: 9px 13px;
  border-radius: 999px;
  transition: color 160ms ease, background 160ms ease;
}
.tip:hover {
  color: var(--ink);
  background: rgba(255, 255, 255, 0.06);
}

/*
 * Half again the type it sits beside, because a cup drawn to the cap height
 * reads as a full stop at this size. Stroked in `currentColor`, so it lifts
 * with the words on hover rather than staying behind while they brighten.
 */
.tip-cup {
  width: 1.4em;
  height: 1.4em;
  flex: none;
}

/* ---------- capture ---------- */

.flash {
  position: absolute;
  inset: 0;
  background: #fff;
  opacity: 0;
  pointer-events: none;
  z-index: 5;
}
.flash.fire { animation: flash 480ms ease-out; }
@keyframes flash {
  0% { opacity: 0.92; }
  100% { opacity: 0; }
}

.countdown {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: "Didot", "Bodoni 72", Georgia, serif;
  font-size: clamp(90px, 22vmin, 240px);
  color: rgba(255, 240, 240, 0.92);
  text-shadow: 0 0 60px rgba(0, 0, 0, 0.8);
  pointer-events: none;
  opacity: 0;
  z-index: 3;
}
.countdown.tick { animation: tick 900ms ease-out; }
@keyframes tick {
  0%   { opacity: 0; transform: scale(1.5); }
  22%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.9); }
}

/* Which frame of the strip is being taken. Sits low and stays put while the
   countdown pulses over the middle of the glass, so the two never collide. */
.shotcount {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 5%;
  text-align: center;
  font-size: clamp(10px, 1.5vmin, 13px);
  letter-spacing: 0.06em;
  color: rgba(255, 235, 232, 0.72);
  text-shadow: 0 0 24px rgba(0, 0, 0, 0.9);
  pointer-events: none;
  z-index: 3;
}

/* ---------- result ---------- */

/*
 * The room stays. This used to be near-opaque black over a 16px blur, which
 * took the washroom out entirely and left the print floating in a void — and
 * the whole conceit is that the booth is standing somewhere. Now it is a soft
 * vignette: clear enough in the middle for the print to sit against, closing in
 * at the edges so the eye still goes to the paper.
 *
 * No backdrop blur either. The room is already blurred 2px in its own layer,
 * and blurring it twice made it a wash of colour rather than a place.
 */
.result {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: radial-gradient(72% 62% at 50% 50%,
              rgba(6, 4, 5, 0.32) 0%, rgba(6, 4, 5, 0.74) 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
  padding: 4vmin;
}

/*
 * What does go is the booth. The room behind the print is the point; the mirror
 * behind it is not, and a slot dispensing paper over the top of a lit mirror
 * reads as two things fighting rather than one machine working.
 */
.booth {
  transition: opacity 260ms ease;
}
body.printing .booth {
  opacity: 0;
  pointer-events: none;
}

/* The mouth the strip comes out of. Sized to the strip by the flow below it,
   and clipping it, so the print is revealed top first as it feeds down. */
.dispenser {
  position: relative;
  overflow: hidden;
  border-radius: 3px;
  /* Room above the strip for the slot itself to sit in. */
  padding-top: 16px;
}

.dispenser .slot {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: min(92vw, 100%);
  height: 7px;
  border-radius: 4px;
  /* A dark recess with a lit lip along the bottom, so it reads as an opening in
     something rather than a line drawn on the backdrop. */
  background: linear-gradient(180deg, #000 0%, #140d0f 62%, rgba(255, 190, 170, 0.28) 100%);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.85), 0 0 22px rgba(255, 110, 70, 0.16);
  z-index: 2;
}

.result img {
  display: block;
  max-width: 80vw;
  /* A strip is taller than it is wide, so height is what constrains it, not width.
     It used to take 78vh, which is a print filling the screen rather than a
     print being handed to you — the room around it is doing work now, and it
     needs somewhere to be. Much below this and it is a postage stamp. */
  max-height: 56vh;
  border-radius: 3px;
  /* Matches the paper `strip.js` fills, so the moment before the PNG decodes
     is the strip in the dark rather than a white flash. */
  background: #0b0809;
  /* A hairline before the drop shadow. Black paper against a dim red room has
     no edge of its own, and without this the print has no outside. */
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.14),
              0 22px 54px rgba(0, 0, 0, 0.72);
}

/* Feeding. Stepped rather than smooth: a printer advances paper a notch at a
   time, and a linear glide reads as a slide transition instead. The whole strip
   plus the slot's own height, so it starts completely inside.

   The step count is the only thing setting how chunky the feed looks. */
.result[data-phase="printing"] img {
  animation: feed var(--feed, 2600ms) steps(26, end) both;
}
@keyframes feed {
  from { transform: translateY(calc(-100% - 16px)); }
  to   { transform: translateY(0); }
}

/* Nothing to press until it has finished coming out. */
.result[data-phase="printing"] .result-actions {
  opacity: 0;
  pointer-events: none;
}
.result-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
  transition: opacity 320ms ease;
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
