:root{
  /* ink and light, taken from the scene rather than invented */
  --ochre:#e8c98f;  --ochre-dim:#a8926a;  --ochre-deep:#7d6a49;
  --ink:#0d0b09;    --ink-soft:#171310;   --rule:#5b4a33;
  --marker:#2b3a5c; --marker-warm:#8a4a2c;
  --lcd:#9dbe57;    --holo:#33b5e5;
  /* type */
  --mono:"IBM Plex Mono",ui-monospace,Menlo,monospace;
  --hand:"Permanent Marker","Bradley Hand","Marker Felt",Chalkboard,cursive;
  /* motion */
  --ease:cubic-bezier(.2,.8,.3,1); --slow:520ms; --quick:180ms;
  /* The left column. Every box and every type size in it derives from this
     one unit, so the column keeps its proportions instead of getting airier
     the bigger the display. It tracks the viewport up to a cap, because a
     wallet that grows without limit starts competing with the window. */
  --col: min(31vw, 48vh, 645px);
  --sleeve: calc(var(--col) * 0.222);
  --gapx: calc(var(--col) * 0.044);
  --gapy: calc(var(--col) * 0.022);
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;background:var(--ink);overflow:hidden;
  font-family:var(--mono);-webkit-font-smoothing:antialiased}
:focus-visible{outline:2px solid var(--ochre);outline-offset:3px;border-radius:3px}

/* isolate: Safari drops mix-blend-mode on a fixed layer unless it blends
   inside a stacking context that also contains the content. */
#frame{position:fixed;inset:0;overflow:hidden;isolation:isolate;background:#07070a}
#stage{position:absolute;inset:0;overflow:hidden}
#scene{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  will-change:transform}

/* --- layers, back to front ------------------------------------------- */
.sky{position:absolute;inset:0;transition:background 700ms ease}
/* THE BLUR STAYS, THE BOX GOES, AND THE MOON MOVES ON THE COMPOSITOR.

   Sahil saw the moon as a soft rectangle on an iPhone on 26 Aug. filter:blur
   on an element is clipped to that element's bounds, and at 26px on a disc
   about 62px across the clip is well inside the glow. Widening the element and
   dividing the gradient's stops by the same number gives the blur room without
   changing the glow, which fixed it while the moon sat still. It still boxed
   WHILE MOVING, because top, left and width were transitioned and every one of
   them re-rasterises the blur mid-flight.

   So the box is fixed and only `transform` changes: translate for where it is,
   scale for how big. A transform is composited, so the blurred disc is
   rasterised once and then moved and scaled as a texture. No re-raster, no
   clip, and the motion is nearly free.

   Drawing the falloff with gradient stops instead was tried and looked worse:
   the blur does not only soften the rim, it spreads and DIMS the disc, so a
   drawn version at full alpha reads as a solid dome with a rim of its own.
   Both attempts are in review/2026-08-26 beside the original.

   SUN_BASE in app.js has to match the 30% here. */
.sun{position:absolute;left:0;top:0;width:30%;padding-bottom:30%;
  border-radius:50%;filter:blur(26px);opacity:.82;pointer-events:none;
  transform-origin:50% 50%;will-change:transform;
  transition:opacity 700ms ease,transform 700ms ease,background 700ms ease}

.band{position:absolute;overflow:hidden;pointer-events:none}
.strip{position:absolute;inset:0;background-repeat:repeat-x;background-size:auto 100%;
  transition:opacity 700ms ease,filter 700ms ease}

/* The lit windows sit over their own skyline and add light rather than paint
   over it. They never take the ramp's dim filter: they are the light source,
   not a surface being lit by one. */
/* positioned by placeBands, because it is taller than the palms band it
   belongs to: the glow above each lamp head has to live somewhere */
.lampglow{position:absolute;background-repeat:repeat-x;background-size:auto 100%;
  pointer-events:none}
.strip.lights,.lampglow.lights{mix-blend-mode:screen;opacity:0;
  transition:opacity 900ms ease;filter:none !important;
  will-change:opacity,background-position}

.haze{position:absolute;inset:0;pointer-events:none;transition:background 700ms ease}

.shimmer{position:absolute;pointer-events:none;opacity:0;
  transition:opacity 700ms ease;
  background:repeating-linear-gradient(0deg,
    rgba(255,255,255,.10) 0px,rgba(255,255,255,0) 3px,rgba(255,255,255,.08) 6px);
  animation:shimmer 2.6s linear infinite}
@keyframes shimmer{0%{transform:translateY(0) scaleY(1)}50%{transform:translateY(-2px) scaleY(1.03)}100%{transform:translateY(0) scaleY(1)}}

#interior,#interior-night,#interior-boy,#interior-boy-night{position:absolute;
  inset:0;width:100%;height:100%;
  display:block;pointer-events:none;transition:filter 700ms ease}

/* ---- the boy breathes -------------------------------------------------
   He is baked into the plate. This plate and the studio's figure-less render
   differ over 77.5% of their pixels, 99.5% of them at the left edge, so they
   are two renders and he cannot be keyed off one onto the other.
   He does not have to leave the plate to move. The plate is drawn a second
   time, clipped to him with a soft mask, and THAT copy breathes. Same pixels,
   same alignment, so there is no seam to hide and no edge to key.
   The mask is deliberately larger than he is: it runs out into the flat wall
   beside him, so its feather never crosses his own silhouette, where a shift
   would show. The glass is cut out of it outright, because a sliver of city
   breathing against a city that scrolls would read as a shimmer.
   Cut out was not enough. The copy travels up to 11.5px on a 1920x1071 plate,
   and the mask ran to within 4px of the glass and hard to the bottom edge at
   full alpha, so its pixels slid against the identical still ones underneath
   and both boundaries doubled: on a 77 inch screen Sahil saw the window frame
   move, a magenta fringe on its border, and the warmth at the bottom shift.
   tools/ease_boy_mask.py puts a dead zone the width of the travel at both, and
   verify-D's D6 pins it against whatever --breath is set to. The mask keeps
   95.9% of its weight, so the breath itself is unchanged.
   Decision 31 is safe: nothing about his framing changes. He is still cropped,
   still half seen, still looking out of the picture. */
#interior-boy,#interior-boy-night{
  -webkit-mask-image:url('assets/web/boy-mask.png');
          mask-image:url('assets/web/boy-mask.png');
  -webkit-mask-size:100% 100%;  mask-size:100% 100%;
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat}
/* Motion is opt-in. With it off the copies sit exactly over the originals and
   cost nothing but a composite, which is the right way for this to degrade. */
@media (prefers-reduced-motion: no-preference){
  #interior-boy,#interior-boy-night{animation:boyBreath 5.4s ease-in-out infinite}
}
/* The rise, as a fraction of the plate's height, so it scales with the layout
   rather than being a pixel count that means something different on a phone.
   It shipped at 0.15%, which is 1.42px on a 951px layer and under a pixel on a
   phone: below the threshold of being felt at all. --breath is a variable so
   the amplitude can be tried live rather than argued about. */
/* 0.7%: about 6.7px of rise on a 950px layer, felt rather than merely
   present. It shipped at 0.15% and could not be felt at all, being under a
   pixel on a phone. Changing it is this one line, and `__breath(0.45)` on the
   running page tries a value live without a rebuild.
     0.15%  1.4px   what shipped first, invisible
     0.45%  4.3px   visible when you look, quiet when you do not
     0.70%  6.7px   this
     1.00%  9.5px   more than a breath
   The scale term rises with it so it stays a breath and not a slide. */
:root{ --breath: 0.7%; --breath-scale: 1.0075; }
@keyframes boyBreath{
  0%,100%{transform:translateY(0) scale(1)}
  52%    {transform:translateY(calc(var(--breath) * -1)) scale(var(--breath-scale))}
}
/* The same breath for the portrait layout, which mirrors the cabin.
   It cannot be done by adding transform:scaleX(-1) to the boy, because an
   ANIMATION'S transform REPLACES a static one rather than composing with it,
   so the flip would be wiped the moment the animation ran and the fix would
   look correct in the stylesheet while doing nothing on screen. The flip has
   to live inside the keyframes.
   translateY is vertical so mirroring does not touch it, and scale() is
   uniform, so scaleX(-1) scale(1.0075) is scaleX(-1.0075) scaleY(1.0075),
   which is the breath, turned round. */
@keyframes boyBreathMirrored{
  0%,100%{transform:scaleX(-1) translateY(0) scale(1)}
  52%    {transform:scaleX(-1) translateY(calc(var(--breath) * -1)) scale(var(--breath-scale))}
}
/* Portrait is a different render with the boy on the other side, so it needs
   its own mask, and it has one now: the same grid reading that told the grade
   where he is. He breathes on a phone too, which matters more than it does on
   a desktop, because decision 20 says portrait IS the experience.
   This mask feathers ACROSS his silhouette rather than beyond it, which is the
   opposite of the landscape one and is right here: in landscape he stands
   against flat wall that a moving edge can hide in, and in portrait he stands
   against the window, where there is nothing to hide in. Feathered across, his
   outline barely moves while the middle of him does, and there is no seam. */
@media (orientation: portrait){
  #interior-boy,#interior-boy-night{
    -webkit-mask-image:url('assets/web/boy-mask-portrait.png');
            mask-image:url('assets/web/boy-mask-portrait.png');
    /* TURN HIM ROUND WITH THE CABIN. Portrait mirrors #interior so the boy
       faces the way the bus travels, and this masked copy of the same plate
       was left facing the other way, so the picture carried TWO boys, back to
       back, from 24 Aug until Sahil saw it on his phone on the 25th. The copy
       arrived with the breath and the flip was never extended to it.
       The mask is cut against the UNFLIPPED plate and is applied in the
       element's own space before the transform, so mirroring the element
       carries image and mask together and the cut-out lands exactly on the
       plate's boy.
       This static flip is what runs when motion is turned off. */
    transform:scaleX(-1)}
}
/* and with motion on, the mirrored breath, because the animation would
   otherwise overwrite the line above */
@media (orientation: portrait) and (prefers-reduced-motion: no-preference){
  #interior-boy,#interior-boy-night{animation-name:boyBreathMirrored}
}
/* The night cabin. The plate we ship is a daylight render, and the evening ramp
   only ever dimmed it: measured, the darkest evening step rendered the cabin at
   hue 20 degrees, still warm, where the studio's own night plate sits at 227.
   A filter cannot walk one to the other, because rotating warm ochre toward
   blue passes through green half way. So the same plate is drawn twice, once
   warm and once cold, and the cold one is faded in. */
/* The night cabin. Landscape has a real one: the day plate repainted facet by
   facet with the colours a night render put in the same places, so every edge
   and the keyed window are the day plate's own and the fade moves nothing.
   Sahil, 24 Aug: going from afternoon to evening the switch should be just the
   light. A second render would have brought its own shadow facets with it, and
   new shapes appearing mid-fade is a change of scene. Only its colour crossed.
   Portrait has its own, made the same way from its own render. Carrying the
   landscape grade across by colour was tried first and loses the direction.
   No filter on either now. The note below is why a filter could never have
   done this, and it stays because it is still true. */
#interior-night,#interior-boy-night{opacity:0;
  transition:opacity 900ms ease,filter 700ms ease}

/* Light falling INSIDE the bus. The ramp used to be a single multiply over
   everything, so the cabin got darker but never got lit from anywhere. These
   add light back with a direction: through the glass, and from a lamp over the
   aisle that stays out of shot. */
.lit{position:absolute;pointer-events:none;opacity:0;
  mix-blend-mode:screen;transition:opacity 900ms ease,background 700ms ease}
/* Each box is deliberately larger than the glow inside it. A radial gradient
   that has not reached transparent by the time it meets the edge of its box
   turns that edge into a visible straight line, which is what the hard seams
   were: #cabin's right edge at scene-x 38%. */
#spill  {left:8%;top:44%;width:60%;height:58%}
#cabin  {left:-20%;top:-30%;width:70%;height:92%}
/* The boy spans scene-x 0.805 to 0.947. The warm edge was at 0.94 to 1.01, so
   nearly all of it fell outside the viewport and the sliver that landed read as
   a bright vertical bar rather than light on a shoulder. */
#rimcool{left:76%;top:10%;width:13%;height:90%}
/* His far side runs off the frame, so there is no contour out there for a rim
   to catch and a blob just fogs his face. The warm light goes on the jaw and
   the top of the shoulder instead, where there is an edge to see it on. */
#rimwarm{left:85%;top:30%;width:9%;height:42%}

/* --- the Discman: a real WebGL object in the empty left third --------- */
#dm{position:absolute;left:0.5%;top:0.5%;
  width:calc(var(--col) * 1.194);height:calc(var(--col) * 0.685);z-index:30;
  -webkit-mask-image:radial-gradient(125% 118% at 46% 44%,#000 62%,transparent 96%);
  mask-image:radial-gradient(125% 118% at 46% 44%,#000 62%,transparent 96%);
  touch-action:none;display:block}

/* Every :hover rule in this file sits inside @media (hover: hover). On a touch
   screen :hover sticks after a tap, so a disc you pressed keeps looking lifted
   and a button you pressed keeps looking lit, which reads as "still selected"
   and fights the .on state that means it. Sahil found this on a phone. */
/* --- the wallet: a 2x3 page, six sleeves ------------------------------- */
/* The box is exactly two sleeves wide and three rows tall, so there is no
   slack for the gap to swallow. It used to be 31vw with the sleeves capped at
   78px, which left 331px between two columns on a 2560 display. */
/* It held five until 23 Aug, so the fifth sleeve spanned both columns and
   centred itself to avoid an orphan in the last row. Six fills the grid, and
   that rule had to go: a sixth item after a full-width fifth starts a FOURTH
   row, which pushed the wallet 47px off the bottom of an 800px viewport. */
/* Centred under the transport rather than hugging the column's left edge. The
   transport spans the full column and the wallet is about half its width, so
   left-aligning the two made the block read bottom-heavy on one side. The box
   itself stays exactly its content size; only its offset changes. */
#wallet{position:relative;
  align-self:center;
  width:calc(var(--sleeve) * 2 + var(--gapx));height:auto;
  display:grid;grid-template-columns:var(--sleeve) var(--sleeve);
  gap:var(--gapy) var(--gapx);
  justify-content:start;align-content:start;user-select:none}
/* --lift is written per sleeve from js while the wallet is hovered: the one
   under the pointer rises furthest and its neighbours follow by how far away
   they are, so the row moves like sleeves in a real wallet rather than one
   card popping out of a still grid. It composes with the three states below
   rather than replacing them, which is why every one of them repeats the
   translateY(var(--lift)) in front: a transform is a list, and the second
   translate is the state while the first is the neighbour response.
   transitions.dev's avatar-group-hover, adapted. Its falloff is over a ROW,
   |i - active|, and this is a 2x3 grid, so the distance is measured in grid
   steps or the sleeve directly below counts as two away instead of one. */
.sleeve{position:relative;width:100%;display:flex;flex-direction:column;
  align-items:center;gap:calc(var(--col) * 0.008);
  cursor:pointer;transition:transform 300ms cubic-bezier(.2,.8,.3,1),filter 300ms;
  transform:translateY(var(--lift,0px)) translateY(4%) scale(.95);
  filter:brightness(.7) saturate(.76)}
@media (hover: hover){
  .sleeve:hover{transform:translateY(var(--lift,0px)) translateY(-2%) scale(1);
    filter:brightness(.92) saturate(.92)}
}
.sleeve.on{transform:translateY(var(--lift,0px)) translateY(-4%) scale(1.04);filter:none}
@media (prefers-reduced-motion:reduce){
  .sleeve{--lift:0px!important}
}
.sleeve svg{display:block;width:100%;aspect-ratio:1.06;overflow:visible}
.sleeve .cap{text-align:center;font-size:calc(var(--col) * 0.021);letter-spacing:.08em;
  font-weight:600;color:#e6cfa2;font-family:var(--mono);white-space:nowrap;
  padding:2px 7px;border-radius:3px;background:rgba(10,8,6,.66);
  text-shadow:0 1px 3px rgba(0,0,0,.95)}
.sleeve.on .cap{color:#fff0c8;background:rgba(58,45,26,.85);
  box-shadow:0 0 0 1px rgba(176,143,92,.5)}

/* a name too long for the panel rolls past, then comes back */
#np1 .rollx,#np2 .rollx{display:inline-block;will-change:transform}
#np1.rolling .rollx,#np2.rolling .rollx{
  animation:rollname var(--dur,9s) ease-in-out infinite}
@keyframes rollname{
  0%,9%    {transform:translateX(0)}
  46%,56%  {transform:translateX(var(--roll,0))}
  93%,100% {transform:translateX(0)}
}
@media (prefers-reduced-motion:reduce){
  #np1.rolling .rollx,#np2.rolling .rollx{animation:none}
  #np1,#np2{text-overflow:ellipsis}
}

/* --- transport: real buttons, not tiny meshes you have to aim at -------- */
/* Fixed height. The iPod skin adds a Now Playing bar the others do not have,
   so a content-sized box grew downward into the wallet. */
/* Transport and wallet now stack in one flow. They used to be two absolutely
   positioned boxes whose separation depended on the transport's reserved
   height still being bigger than its contents, which stopped being true as
   soon as the column got small: the light toggle landed 34px into the wallet
   on a landscape phone and 51px in portrait. A flex column cannot overlap. */
#column{position:absolute;left:2%;top:35.5%;width:var(--col);z-index:31;
  display:flex;flex-direction:column;gap:calc(var(--col) * 0.030)}
#transport{width:100%;display:flex;flex-direction:column}
/* Reserved, not greedy. It still holds the tallest skin's title bar, but it no
   longer swallows every spare pixel: at 2560 that left the panel 69% empty. */
#transport .np{flex:0 0 auto;margin:0 0 .45rem;overflow:hidden;
  display:flex;flex-direction:column;justify-content:center;gap:.1rem}
#np1{font:600 calc(var(--col) * 0.0325)/1.3 var(--mono);color:#f1dcae;
  overflow:hidden;white-space:nowrap;
  text-shadow:0 1px 4px rgba(0,0,0,.7)}
#np2{font:500 calc(var(--col) * 0.0262)/1.3 var(--mono);color:#a8926a;
  overflow:hidden;white-space:nowrap}
/* the position row: it is what makes press-and-hold legible */
#transport .pb{display:flex;align-items:center;gap:.5rem;margin-top:.35rem;
  font:500 calc(var(--col) * 0.0218)/1 var(--mono);color:#a8926a;
  font-variant-numeric:tabular-nums}
#transport .pb .bar{flex:1;height:3px;border-radius:2px;background:rgba(255,255,255,.16);
  overflow:hidden}
#transport .pb .bar i{display:block;height:100%;width:0;background:var(--ochre);
  border-radius:2px;transition:width 120ms linear}
#transport.scrubbing .pb .bar i{transition:none;background:#fff0c8}
#transport.scrubbing .pb{color:#f1dcae}

#transport .tp{flex:0 0 auto;display:flex;gap:clamp(6px,.8vw,12px)}
#bShuf::before{content:'RND'}
#transport button{flex:1;padding:clamp(8px,1vh,14px) 0;cursor:pointer;
  min-height:2.2em;
  font:600 calc(var(--col) * 0.0315)/1 var(--mono);color:#e8d3a4;
  background:linear-gradient(#3a3025,#241d15);border:1px solid #5b4a33;border-radius:6px;
  box-shadow:0 2px 0 #17120c,0 6px 14px rgba(0,0,0,.45);
  transition:transform 90ms,background 160ms,border-color 160ms}
@media (hover: hover){
  #transport button:hover{background:linear-gradient(#4a3d2d,#2c231a);border-color:#8a6f49}
}
#transport button:active{transform:translateY(2px);box-shadow:0 0 0 #17120c,0 3px 8px rgba(0,0,0,.5)}
#transport button:focus-visible{outline:2px solid #f0d08a;outline-offset:2px}

/* --- the transport wears the interface of its era ---------------------- */
#tod{flex:0 0 auto;display:flex;gap:6px;margin-top:.5rem}
#tod button{flex:1;padding:.46rem 0;cursor:pointer;border-radius:5px;
  font:600 calc(var(--col) * 0.0248)/1 var(--mono);letter-spacing:.09em;
  background:rgba(12,10,7,.92);color:#dcc49a;
  border:1px solid #645136;transition:all 160ms}
@media (hover: hover){
  #tod button:hover{color:#f6e2b4;border-color:#8a6f49;background:rgba(30,24,16,.95)}
}
#tod button.on{background:#4a3d2b;color:#ffeec2;border-color:#b08f5c;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.12)}
@media (hover: hover){
  #tod button:hover{color:#d8c090}
}

/* ---- Discman: a segment LCD sunk behind a plastic window --------------- */
#transport[data-era="cd"] .np{background:#9dbe57;border:2px solid #2a2f22;border-radius:3px;
  padding:.45rem .55rem;box-shadow:inset 0 3px 8px rgba(0,0,0,.32),0 1px 0 rgba(255,255,255,.12);
  background-image:repeating-linear-gradient(0deg,rgba(0,0,0,.05) 0 1px,transparent 1px 4px),
    linear-gradient(#a6c661,#8fb04c)}
#transport[data-era="cd"] #np1{color:#141d0b;text-shadow:none;letter-spacing:-.01em}
#transport[data-era="cd"] #np2{color:#3a4f1d}
#transport[data-era="cd"] .pb{color:#3a4f1d}
#transport[data-era="cd"] .pb .bar{background:rgba(20,29,11,.22)}
#transport[data-era="cd"] .pb .bar i{background:#16210d}
#transport[data-era="cd"].scrubbing .pb .bar i{background:#2f4a12}
#transport[data-era="cd"] button{background:linear-gradient(#4a4034,#2a231a);
  border:1px solid #6b5a41;color:#e8d3a4;border-radius:5px;
  box-shadow:0 2px 0 #16110b,inset 0 1px 0 rgba(255,255,255,.14)}
@media (hover: hover){
  #transport[data-era="cd"] button:hover{background:linear-gradient(#5b4e3f,#332a20)}
}

/* ---- iPod: pale chrome, a title bar, Helvetica, glossy white keys ------ */
/* The nano shares the Classic's skin, and that is a fact about the object
   rather than a shortcut. An iPod nano 4th generation ran the same iPod OS as
   the Classic of the same year: same pale blue-grey chrome, same Now Playing
   bar, same pill scrubber. Decision 11 asks the transport to wear the
   interface of its era, and in 2011-2013 that interface was this one. Before
   this, data-era="nano" matched no rule at all and the panel fell back to the
   unstyled default. */
#transport[data-era="mp3"] .np, #transport[data-era="nano"] .np{background:linear-gradient(#f5f8fb,#d2dbe5);
  border:1px solid #94a7ba;border-radius:5px;padding:0;overflow:hidden;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.95),0 1px 3px rgba(0,0,0,.35)}
#transport[data-era="mp3"] .np, #transport[data-era="nano"] .np{justify-content:flex-start}
#transport[data-era="mp3"] .np::before, #transport[data-era="nano"] .np::before{content:'Now Playing';display:block;
  flex:0 0 auto;background:linear-gradient(#a3b5c8,#7d94ad);color:#fff;
  font:600 clamp(9px,.7vw,10.5px)/1.85 "Helvetica Neue",Helvetica,Arial,sans-serif;
  text-align:center;border-bottom:1px solid #6b8299;
  text-shadow:0 -1px 0 rgba(0,0,0,.25)}
#transport[data-era="mp3"] #np1, #transport[data-era="nano"] #np1{padding:.3rem .55rem 0;color:#16283c;text-shadow:none;
  font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:700}
#transport[data-era="mp3"] #np2, #transport[data-era="nano"] #np2{padding:.02rem .55rem .3rem;color:#5a7490;
  font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500}
#transport[data-era="mp3"] .pb, #transport[data-era="nano"] .pb{color:#5a7490;padding:0 .55rem .32rem;
  font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}
#transport[data-era="mp3"] .pb .bar, #transport[data-era="nano"] .pb .bar{height:5px;border-radius:3px;background:#b9c5d1}
#transport[data-era="mp3"] .pb .bar i, #transport[data-era="nano"] .pb .bar i{background:#5d7d9e}
#transport[data-era="mp3"] #bShuf::before, #transport[data-era="nano"] #bShuf::before{content:'Shuffle'}
#transport[data-era="mp3"] button, #transport[data-era="nano"] button{background:linear-gradient(#ffffff,#dde5ed);
  border:1px solid #97a9bb;color:#3c4c5e;border-radius:999px;
  box-shadow:inset 0 1px 0 #fff,0 2px 0 #a6b4c2,0 5px 12px rgba(0,0,0,.26)}
@media (hover: hover){
  #transport[data-era="mp3"] button:hover, #transport[data-era="nano"] button:hover{background:linear-gradient(#fff,#eaf0f6);border-color:#748ba1}
}
#transport[data-era="mp3"] #tod button, #transport[data-era="nano"] #tod button{background:linear-gradient(#f7fafd,#d8e2ec);
  border:1px solid #8fa2b5;color:#20344a;border-radius:999px;letter-spacing:0;
  font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:600}
#transport[data-era="mp3"] #tod button.on, #transport[data-era="nano"] #tod button.on{background:linear-gradient(#7e9ab4,#5b7a97);
  color:#fff;border-color:#4a6580;
  text-shadow:0 -1px 0 rgba(0,0,0,.35)}

/* ---- Galaxy S3: Android 4 holo. near black, Roboto, #33b5e5 ----------- */
#transport[data-era="phone"] .np{background:#0a0d12;border:1px solid #1d2530;
  border-top:2px solid #33b5e5;border-radius:2px;padding:0}
#transport[data-era="phone"] #np1{display:block;padding:.45rem .6rem 0;color:#f2f6fa;
  font-family:Roboto,"Helvetica Neue",Arial,sans-serif;font-weight:400;text-shadow:none}
#transport[data-era="phone"] #np2{display:block;padding:.08rem .6rem .5rem;color:#7f94ab;
  font-family:Roboto,"Helvetica Neue",Arial,sans-serif;font-weight:300}
#transport[data-era="phone"] .pb{color:#7f94ab;padding:0 .6rem .45rem;
  font-family:Roboto,"Helvetica Neue",Arial,sans-serif}
#transport[data-era="phone"] .pb .bar{height:2px;border-radius:0;background:rgba(255,255,255,.18)}
#transport[data-era="phone"] .pb .bar i{border-radius:0;background:var(--holo)}
#transport[data-era="phone"] #bShuf::before{content:'\21C4'}
#transport[data-era="phone"] button{background:#11161d;border:1px solid #222b36;
  color:#dfe9f2;border-radius:2px;box-shadow:none;
  font-family:Roboto,"Helvetica Neue",Arial,sans-serif;font-weight:400}
@media (hover: hover){
  #transport[data-era="phone"] button:hover{background:#18202a;border-color:#33b5e5;color:#fff}
}
#transport[data-era="phone"] button:active{transform:translateY(1px)}
#transport[data-era="phone"] #tod button{background:#161d26;border:1px solid #35404e;
  color:#cfdcea;border-radius:2px;letter-spacing:.04em;
  font-family:Roboto,"Helvetica Neue",Arial,sans-serif;font-weight:400}
@media (hover: hover){
  #transport[data-era="phone"] #tod button:hover{background:#18202a;color:#e6eef7;border-color:#33b5e5}
}
#transport[data-era="phone"] #tod button.on{background:#12303f;color:#8bdcff;border-color:#33b5e5}

/* --- attribution, required by CC-BY on any downloaded model ------------ */
/* This is the attribution CC-BY obliges the page to carry, so it has to be
   legible over a mid-tone bus seat rather than merely present. Its own plate,
   full opacity, and it sits above a button that never moves. */
#credits{position:fixed;right:12px;bottom:46px;opacity:0;z-index:50;color:#e0cca4;
  font-size:11px;letter-spacing:.04em;font-family:var(--mono);
  text-align:right;line-height:1.65;pointer-events:auto;
  background:rgba(9,7,5,.92);border:1px solid #2c2419;border-radius:4px;
  padding:.55rem .7rem}
#credits a{color:#fff0c8}
/* The music line is a thank-you, not a licence term, so it sits apart from the
   four that are. It used to hang off the nano's CGTrader line after a middle
   dot, which read as one clause covering both. */
#credits .thanks{display:inline-block;margin-top:.5em;padding-top:.5em;
  border-top:1px solid rgba(224,204,164,.22);color:#c3ae86}
body[data-started] #credits{opacity:0;pointer-events:none;transform:translateY(6px)}
body[data-started] #credits.open{opacity:1;pointer-events:auto;transform:none}
#credits{transform:translateY(6px);transition:opacity 320ms var(--ease),transform 320ms var(--ease)}
#creditsbtn{position:fixed;right:12px;bottom:10px;z-index:51;cursor:pointer;opacity:0;
  font-family:var(--mono);font-size:10px;letter-spacing:.14em;color:var(--ochre-deep);
  background:rgba(12,10,8,.55);border:1px solid #2c2419;border-radius:3px;padding:.34rem .62rem;
  transition:opacity 320ms var(--ease),color var(--quick),border-color var(--quick)}
body[data-started] #creditsbtn{opacity:1}
@media (hover: hover){
  #creditsbtn:hover{color:var(--ochre);border-color:var(--rule)}
}
#creditsbtn[aria-expanded="true"]{color:var(--ochre);border-color:var(--rule)}
/* removed: #credits.open + #creditsbtn{bottom:auto} sent the button to y=0,
   400px from the panel it toggles, because a fixed box with neither top nor
   bottom falls back to its static position. The credits sit above it now. */

/* --- grain sits ABOVE everything, including the phone ----------------- */
#grain{position:absolute;inset:0;pointer-events:none;z-index:40;
  mix-blend-mode:overlay;opacity:.48}

/* --- the way in: the scene is already running, so nothing is hidden ----- */
#intro{position:fixed;inset:0;z-index:55;font-family:var(--mono)}
#intro.gone{pointer-events:none}
#intro .scrim{position:absolute;inset:0;
  background:radial-gradient(120% 90% at 62% 46%,rgba(10,8,6,.30) 0%,rgba(8,6,5,.74) 78%);
  transition:opacity var(--slow) var(--ease)}

/* rings and leaders, drawn in the same pen as the disc labels */
#marks{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
#marks path{stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}
@keyframes inkIn{from{opacity:0} to{opacity:.78}}
.ringc{position:absolute;pointer-events:none;opacity:0;
  animation:inkIn 460ms var(--ease) 100ms both}
#intro.gone .ringc{animation:noteOut 300ms var(--ease) both}
#marks .lead{fill:none;stroke:var(--ochre);stroke-width:1.7;opacity:.45;
  stroke-dasharray:1200;stroke-dashoffset:1200;
  animation:draw 620ms var(--ease) 140ms both}
@keyframes draw{to{stroke-dashoffset:0}}

#notes{position:absolute;inset:0;pointer-events:none}
#notes .note{position:absolute;transform:translateY(-50%);font-family:var(--hand);
  color:var(--ochre);font-size:clamp(12px,1.02vw,17px);line-height:1.5;
  text-shadow:0 2px 10px rgba(0,0,0,.9);opacity:0;
  animation:noteIn 520ms var(--ease) 180ms both}
#notes .note em{font-style:normal;color:var(--ochre-dim);font-size:.86em}
@keyframes noteIn{to{opacity:1}}

/* the plate sits low right, over the seat, so the window stays clear */
#intro .plate{position:absolute;right:5%;bottom:7%;max-width:min(30ch,32vw);text-align:left;
  padding:1.6rem 1.8rem 1.5rem;border-radius:4px;
  background:radial-gradient(130% 120% at 82% 60%,rgba(9,7,5,.86) 0%,rgba(9,7,5,0) 76%);
  opacity:0;animation:noteIn 560ms var(--ease) 140ms forwards}
/* the title is an actual disc: real geometry, real thin-film iridescence */
#intro h1.cdtitle{width:clamp(170px,18vw,272px);aspect-ratio:1;margin:0 0 .45rem;
  transform:rotate(-3deg);
  filter:drop-shadow(0 20px 34px rgba(0,0,0,.62)) drop-shadow(0 5px 10px rgba(0,0,0,.45))}
#intro h1.cdtitle canvas{display:block;width:100%;height:100%;cursor:grab;
  touch-action:none;pointer-events:auto}
#intro h1.cdtitle canvas.held{cursor:grabbing}

#intro .blurb{margin:1.15rem 0 1.6rem;color:#c1ab84;font-size:clamp(11px,.88vw,13.5px);
  line-height:1.95;letter-spacing:.015em;text-wrap:pretty;max-width:26ch}
#intro .blurb .kick{display:inline-block;margin-top:.55rem;color:var(--ochre);}
#intro .fine{margin-top:1rem;color:var(--ochre-deep);max-width:26ch;font-size:clamp(9px,.72vw,11px);
  letter-spacing:.06em}
/* The one thing on this screen to press, so it is a key rather than a plaque:
   a lit top edge, a graded face and a well to sink into.

   It was a pale grey "moulded plastic" for about an hour and Sahil rejected it
   by eye before any measurement did. The measurement agreed: the scene sits at
   hue 33 and that grey at hue 211, 178 degrees away at 7% saturation, and
   there is no cool grey anywhere in the token block at the top of this file.
   The excuse for it, that a Discman's keys are grey, did not survive either:
   the D-121 as this piece renders it measures hue 335, saturation 55, value
   6.5, which is a near-black warm body. The moulding was the good part. The
   colour is the room's. */
#startbtn{position:relative;overflow:hidden;
  font-family:var(--mono);font-size:clamp(12.5px,1.05vw,15.5px);font-weight:600;
  letter-spacing:.2em;color:#f2dfb2;cursor:pointer;
  padding:1rem 2.4rem;border:1px solid #6d5836;border-radius:5px;
  background:linear-gradient(#4c3f2d 0%,#382d20 48%,#261e16 100%);
  box-shadow:inset 0 1px 0 rgba(255,238,205,.34),
             inset 0 -1px 0 rgba(0,0,0,.40),
             0 3px 0 #17120c,0 10px 22px rgba(0,0,0,.55);
  transition:transform 90ms var(--ease),background var(--quick),
             border-color var(--quick),box-shadow 90ms var(--ease)}
/* The play mark, drawn rather than set as the character. A mono face may not
   carry U+25B6 and will substitute it at another size and baseline. */
#startbtn::before{content:'';display:inline-block;vertical-align:-1px;
  width:0;height:0;margin-inline-end:.85em;
  border-top:5.5px solid transparent;border-bottom:5.5px solid transparent;
  border-inline-start:9px solid #f2dfb2}
/* The sweep: a skewed band of light parked off the left edge, crossing the
   face once per hover. Amicro's glare, which is MIT and is React on Framer
   Motion, so the idea came across and the code could not. Ochre, not white: a
   white highlight on a warm key is the same mistake as the grey was.
   pointer-events:none so it can never eat the click it lies on top of. */
#startbtn::after{content:'';position:absolute;top:-60%;bottom:-60%;width:34%;
  left:-45%;pointer-events:none;transform:skewX(-18deg);
  background:linear-gradient(90deg,rgba(255,240,210,0) 0%,
    rgba(255,240,210,.30) 50%,rgba(255,240,210,0) 100%)}
@media (hover: hover){
  #startbtn:hover{background:linear-gradient(#5d4e38 0%,#453728 48%,#2f2519 100%);
    border-color:#8a6f49;color:#fbecc6}
  #startbtn:hover::before{border-inline-start-color:#fbecc6}
}
/* Motion is opt-in, not opt-out, so anything added here later is quiet by
   default rather than loud until someone remembers the guard. */
@media (hover: hover) and (prefers-reduced-motion: no-preference){
  #startbtn:hover::after{animation:startSweep 720ms var(--ease) 1}
}
@keyframes startSweep{ to{ left:118% } }
#startbtn:active{transform:translateY(3px);
  box-shadow:inset 0 2px 5px rgba(0,0,0,.45),
             0 0 0 #17120c,0 3px 8px rgba(0,0,0,.5)}
#startbtn[disabled]{opacity:.45;cursor:progress;filter:saturate(.4)}

#intro.gone .scrim{opacity:0;transition:opacity var(--slow) var(--ease)}
#intro.gone .note,#intro.gone .plate{animation:noteOut 300ms var(--ease) both}
#intro.gone #marks .ring{animation:noteOut 320ms var(--ease) both}
#intro.gone #marks .lead{animation:noteOut 300ms var(--ease) both}
#intro.gone .mark path{animation:markOut 300ms var(--ease) both}
@keyframes noteOut{from{opacity:1}to{opacity:0;transform:translateY(-5px)}}
@keyframes markOut{from{stroke-dashoffset:0;opacity:.66}to{stroke-dashoffset:-180;opacity:0}}

/* bring the notes back later without leaving the scene */
#helpbtn{position:fixed;left:14px;bottom:12px;z-index:52;cursor:pointer;
  font-family:var(--mono);font-size:11px;letter-spacing:.12em;color:var(--ochre-deep);
  background:rgba(12,10,8,.6);border:1px solid #2c2419;border-radius:3px;
  padding:.4rem .7rem;transition:color var(--quick),border-color var(--quick)}
@media (hover: hover){
  #helpbtn:hover{color:var(--ochre);border-color:var(--rule)}
}

@media (prefers-reduced-motion:reduce){
  #intro .note,#intro .plate{animation:none;opacity:1;transform:none}
  #intro .arrow path{animation:none;stroke-dashoffset:0}
  #startbtn:active{transform:none}
  #startbtn::after{animation:none}
}
@media (max-width:900px){
  #intro .note{font-size:12px}
  #intro .n1{top:9%} #intro .n2{top:33%} #intro .n3{top:44%} #intro .n4{top:57%}
  #intro .plate{right:4%;bottom:5%;max-width:60vw}
}

#yt{position:fixed;width:1px;height:1px;left:-9999px;top:-9999px;opacity:0}


/* ---- a phone ----------------------------------------------------------
   Landscape is the real thing: the composition survives rotation intact, and
   every plate is reused. Portrait cannot show it, because the window and the
   boy sit side by side, so portrait gets an honest fallback instead of a
   silent crop. iOS Safari has no orientation lock and no Fullscreen API, so
   the piece asks rather than forces. */
@media (pointer: coarse){
  /* a finger is not a mouse pointer */
  #transport .tp button, #tod button{min-height:44px}
  #bShuf,#bPrev,#bPlay,#bNext{min-height:44px}
  .sleeve{min-width:44px}
  #helpbtn,#creditsbtn{padding:.7rem 1rem}
}

@media (orientation: landscape) and (max-height: 560px){
  /* short and wide: height is the scarce thing, so the column reads off it */
  /* The canvas has to finish above #column, which starts at 26% of the
     viewport. Sized off --col it ran 55px into the transport on an 874x402
     phone and the Discman was drawn behind the panel, cut in half. Height
     leads now and the width follows it, so the machine keeps its proportions
     and always lands clear of the panel. */
  :root{--col:min(30vw, 96vh, 300px);
        --dmh:min(23vh, calc(var(--col) * 0.60))}
  #column{top:26%}
  /* Centred on the column's axis, not pinned to the left edge. Shrinking the
     canvas while it still started at 0.5% left the machine 63px off the centre
     line of the panel and wallet under it. */
  #dm{left:calc(2% + (var(--col) - var(--dmh) * 1.75) / 2);
      top:calc((26% - var(--dmh)) / 2);
      height:var(--dmh);width:calc(var(--dmh) * 1.75)}
  #transport .np{margin-bottom:.3rem}
  #transport .pb{margin-top:.25rem}
  #tod{margin-top:.32rem}
  /* Six across rather than three down: a short screen has width to spare.
     The tracks are fractions, not var(--sleeve): fixed sleeves plus their gaps
     came to 310px inside a 253px column and hung off the right edge. */
  #wallet{width:100%;
    grid-template-columns:repeat(6, 1fr);
    gap:var(--gapy) calc(var(--col) * 0.024)}
}

/* ---- the sheet -------------------------------------------------------
   On a phone the window and the machine cannot both be the hero, and the DOM
   panel is a second copy of a readout the device's own screen already carries.
   So portrait shows the scene and the machine, and summons the rest. */
/* above the sheet, not below it: the handle is also its close control, and
   the sheet was covering the very thing you tap to put it away */
#handle{position:fixed;left:0;right:0;bottom:0;z-index:36;
  display:none;flex-direction:column;align-items:center;gap:.45rem;
  padding:.7rem 1rem calc(.9rem + env(safe-area-inset-bottom));
  background:linear-gradient(rgba(10,8,6,0),rgba(10,8,6,.92) 42%);
  border:0;cursor:pointer;font-family:var(--mono);
  font-size:11px;letter-spacing:.16em;color:var(--ochre);
  text-shadow:0 1px 4px rgba(0,0,0,.9)}
/* Not a grip bar. A 42x4 pill at the bottom of a phone screen is the iOS home
   indicator, and it reads as "swipe up", which this is not: it is a button.
   A chevron that turns over says which way the panel will go. */
#handle .chev{display:block;width:18px;height:18px;color:var(--ochre-dim);
  transition:transform 300ms var(--ease),color 200ms linear}
#handle .chev svg{display:block;width:100%;height:100%}
body[data-sheet="open"] #handle .chev{transform:rotate(180deg);color:var(--ochre)}
@media (hover: hover){
  #handle:hover .chev{color:var(--ochre)}
}

@media (orientation: portrait){
  /* Portrait is its own composition now, not a letterboxed landscape one. The
     scene covers the frame, and #dm and #column are placed from js in scene
     coordinates so the device lands on the seat back and the wallet sits in
     the seat pocket, the way they rest on things in landscape. */
  :root{--col:min(90vw, 40vh, 460px)}
  #handle{display:flex}
  /* `hidden` is display:none from the UA sheet, which an id rule outranks, so
     the handle sat under the title page until start() ran. */
  #handle[hidden]{display:none}
  /* the controls live in a sheet that comes up when asked for */
  #column{position:fixed;left:0;right:0;bottom:0;top:auto;width:100%;
    z-index:34;gap:calc(var(--col) * 0.020);
    /* clears the handle with room to spare: at 3.4rem the bottom row of
       sleeves finished 2px above it */
    padding:1.1rem 5% calc(5.4rem + env(safe-area-inset-bottom));
    background:linear-gradient(rgba(10,8,6,0),rgba(9,7,5,.97) 14%);
    transform:translateY(102%);
    transition:transform 340ms var(--ease);
    will-change:transform}
  body[data-sheet="open"] #column{transform:translateY(0)}
  body[data-sheet="closed"] #column{pointer-events:none}
  /* tapping the scene closes it */
  body[data-sheet="open"] #frame::after{content:'';position:fixed;inset:0;z-index:32}
  #wallet{width:100%;
    grid-template-columns:repeat(6, 1fr);
    gap:var(--gapy) calc(var(--col) * 0.016)}
  /* the ceiling is the top of the frame: chrome lives there, nothing critical */
  #credits{bottom:auto;top:calc(8px + 2.4rem);right:8px;max-width:74vw;font-size:10px}
  #creditsbtn{bottom:auto;top:8px;right:8px}
  #helpbtn{left:8px;top:8px;bottom:auto}
  /* the device rests on a surface, so it should cast onto it */
  #dm{-webkit-mask-image:radial-gradient(128% 122% at 50% 46%,#000 66%,transparent 98%);
      mask-image:radial-gradient(128% 122% at 50% 46%,#000 66%,transparent 98%)}
  /* The portrait plate is a separate render and its boy faces the other way.
     The world scrolls left, measured at 29px in 4s, so the bus travels right
     and the landscape boy faces the way it is going. Portrait had him facing
     backwards. Mirroring the plate turns him round. The window hole spans the
     full width here (WIN.x 0.0, w 0.9997), so mirroring moves it half a pixel
     and nothing else has to change. */
  #interior,#interior-night{transform:scaleX(-1)}
}

/* The rotate ask is gone. Portrait is its own composition, not a fallback, so
   asking people to turn the phone told them the thing they were looking at was
   second best. Landscape is a discovery for anyone who turns it anyway. */


/* ---- the title page on a phone ---------------------------------------
   The disc is the hero here. At `clamp(170px,18vw,272px)` it fell to its 170px
   floor on a 402px screen, which is 42% of the width, and the plate was still
   anchored bottom-right at 32vw, so the copy ran in a narrow column beside it.
   Portrait has one column and the whole width, so use it: the disc takes 70vw,
   the copy sits under it, and everything is centred. No transform is used for
   the centring, because #intro.gone animates `transform` on the exit and would
   fight it. */
@media (orientation: portrait){
  #intro .plate{left:7vw;right:auto;top:13vh;bottom:auto;width:86vw;
    max-width:none;text-align:center;padding:0;
    background:radial-gradient(115% 78% at 50% 42%,rgba(9,7,5,.88) 0%,rgba(9,7,5,0) 76%)}
  #intro h1.cdtitle{width:min(70vw,40vh);margin:0 auto .2rem}
  #intro .blurb{margin:1.5rem auto 1.7rem;max-width:32ch;
    font-size:clamp(12.5px,3.5vw,15px);line-height:1.9}
  #intro .blurb .kick{margin-top:.7rem}
  #startbtn{font-size:clamp(13px,3.6vw,16px);padding:1rem 2.6rem;letter-spacing:.24em}
  #intro .fine{margin:1.3rem auto 0;max-width:30ch;font-size:clamp(9.5px,2.7vw,11.5px)}
  /* the pointed-out annotations need room the phone has not got, and the disc
     is the whole point of this screen. "what's what" still brings them back. */
  #intro #notes,#intro #marks{display:none}
}