/* style.css — the whole visual language of the page, in one file.
 *
 * Off-white paper, near-black ink, one accent per person, and text kept small and
 * quiet so the two scenes are the only thing with any colour in it. Every
 * object in a scene is a sprite — original art, drawn by the scripts in
 * tools/pixelart/ — and what is left for CSS is what isn't an object: walls,
 * ground, light and the hour's tint. (And Kimchi, who predates the sprites.)
 *
 * The type is the platform's own sans (SF, Segoe, Roboto, ...), not a
 * webfont: the page is built to work opened straight off a disk with no network
 * at all, so nothing is fetched. The pixel art is the only thing on the page
 * drawn in pixels; the text around it stays clean.
 */

:root{
  --paper: #f9f7f2;
  --ink: #2f2b2e;
  --muted: #6d6669;
  --rule: #c9c5c0;

  /* One accent per person. Set again per .theme-* below, so a panel's links
     and focus rings pick up their own colour without knowing whose they are. */
  --accent: #8fbf62;

  --sky: #a9d6e5;
  --grass: #8fbf62;
  --grass-dark: #6f9c4a;
  --wall: #f3ecdf;
  --wall-band: #e6dcc8;
  --floorboard: #c39a68;

  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 18px;
  --sp-5: 26px;
  --sp-6: 40px;

  --text-xs: 10px;
  --text-sm: 13px;
  --text-lg: 28px;

  --font: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    'Helvetica Neue', Arial, sans-serif;
}

.theme-hannah{ --accent: #8fbf62; }
.theme-martin{ --accent: #e2853f; }

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

html{ background: var(--paper); }

body{
  margin: 0;
  padding: var(--sp-6) clamp(var(--sp-4), 4vw, 48px);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-sm);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  /* Tracking belongs to the small uppercase labels, which set their own;
     running text and names are set as the face was drawn. */
  letter-spacing: normal;
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  min-height: 100vh;
}

h1, h2, h3, h4{ margin: 0; font-weight: 400; }
p{ margin: 0; }

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

.skip-link{
  position: absolute;
  left: -9999px;
  top: var(--sp-2);
  background: var(--ink);
  color: var(--paper);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--text-xs);
  text-decoration: none;
  z-index: 50;
}
.skip-link:focus{ left: var(--sp-3); }

main:focus{ outline: none; }

/* --- Main: the introduction, then the two worlds ------------------------ */

main{
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}

/* --- Portrait ----------------------------------------------------------- */
/* The crop window is exactly the size of the drawn figure, scaled by a whole
   number; portrait.js sets the sizes and offsets, because they come from each
   character's own sheet. Nothing here is allowed to size the sprite —
   `width: 100%` on a portrait would be a fractional scale and mush. */

.portrait{
  position: relative;
  display: block;
  flex: none;
  overflow: hidden;
}
.portrait-sheet{
  position: absolute;
  background-repeat: no-repeat;
  image-rendering: pixelated;
  /* Duration, step count and the width of one cycle are set inline from the
     character's grid. The name lives here so the reduced-motion block below
     can switch it off. */
  animation-name: portrait-step;
  animation-iteration-count: infinite;
}
/* Frame stepping, not sliding: steps() means the sheet jumps a whole cell at a
   time, exactly like the avatars' walk cycle on the stage. */
@keyframes portrait-step{
  from{ background-position-x: 0; }
  to  { background-position-x: var(--portrait-span, 0); }
}

/* --- The two worlds, on a diagonal -------------------------------------- */
/* Each person is one block: their world, and beside it everything that is
   about them. The first block puts its world on the left, the second on the
   right, so the page reads top-left to bottom-right rather than as two matched
   columns — and each world gets most of a full-width row to itself instead of
   half of one, which is what lets it be drawn big.
   Which column a world lands in is set by placement rather than source order,
   so the DOM can keep each name and its links ahead of the stage. */

.worlds{
  display: grid;
  gap: clamp(var(--sp-6), 6vw, 88px);
  align-content: start;
}

.world{
  display: grid;
  /* The world takes whatever is left; the text column is capped so it never
     stretches into prose. */
  grid-template-columns: minmax(0, 1fr) minmax(11rem, 17rem);
  align-items: center;
  gap: clamp(var(--sp-4), 3vw, 40px);
  min-width: 0;
}
.world > .card-stage{ grid-column: 1; grid-row: 1; min-width: 0; }
.world > .world-meta{ grid-column: 2; grid-row: 1; }

/* The second world mirrors the first: its text to the left, its world right. */
.world--right{ grid-template-columns: minmax(11rem, 17rem) minmax(0, 1fr); }
.world--right > .card-stage{ grid-column: 2; }
.world--right > .world-meta{ grid-column: 1; }

/* --- The takeover: one wide scene, both of them in it -------------------- */
/* At the hours scenes.js says the two of them are out together, the page stops
   being two worlds and becomes one. It is a change of what is SHOWN, not of
   the DOM: no element moves, so the reading order and the tab order are
   whatever the markup says in both arrangements.
   What must never happen here — and the reason this is a stack of `display`
   rules rather than a second layout — is either person's name, CV button or
   email link going away. This is a CV page. The bar is what the two
   of them are doing tonight; the links are what the page is for. So both
   worlds keep their whole meta block, and it is only the two now-empty stages
   that stand down. */

.together{ display: none; }
.together .scene-label{ margin-bottom: var(--sp-3); }

body.is-together .together{ display: block; }
body.is-together .world > .card-stage{ display: none; }
/* Each world's own scene label is describing a room nobody is in for the next
   few hours. The bar has its own, over the wide stage. */
body.is-together .world-meta .scene-label{ display: none; }
/* With its stage gone, a world is a single column of text — otherwise the meta
   block stays parked in whichever column it was placed in and sits indented
   against an empty one. */
body.is-together .world,
body.is-together .world--right{ grid-template-columns: minmax(0, 1fr); }
body.is-together .world > .world-meta,
body.is-together .world--right > .world-meta{ grid-column: 1; grid-row: 1; }

/* The three blocks stack in source order: her, the bar, him. That was tried
   the other way first — a text column flanking the scene on each side — and
   the arithmetic kills it. The stage scales by whole numbers only, so a scene
   336 logical pixels wide needs 1008 CSS pixels to reach 3x; two text columns
   and their gaps eat about 430 of a 1440px page before the bar sees any, which
   drops it to 2x and leaves the shared scene visibly SMALLER than either world
   is on a normal evening. Stacked, the bar gets the full width of the page and
   reads like the event it is meant to be.
   What it costs is vertical room, and that is bought back below: with no stage
   beside it a meta block does not need to be a column at all, so it becomes a
   compact strip — portrait, name, links, in a row. */
body.is-together .worlds{ gap: clamp(var(--sp-4), 3vw, 40px); }
body.is-together .world-meta{
  flex-direction: row;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-4);
}
body.is-together .world-meta .portrait{ margin-bottom: 0; }
body.is-together .person-links{ padding-top: 0; }

.world-meta{
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-1);
  min-width: 0;
}
/* The portrait stands on the name rather than beside it, so the column stays
   narrow enough to leave the world its room. */
.world-meta .portrait{ margin-bottom: var(--sp-2); }
/* In a column this narrow the label, the count and the clock need to be free
   to wrap onto a second line instead of squeezing each other (the wrap itself
   is on .scene-label now — every one of them needs it). */
.world-meta .scene-label{ margin-top: var(--sp-3); }

.person-name{
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.2;
}

.person-links{
  display: flex;
  gap: var(--sp-4);
  padding-top: var(--sp-2);
}

/* CV and Email are the same thing structurally — one is a button because it
   opens a panel, one is a link because it leaves the page — so they are styled
   as one and differ only in weight of underline. */
.link{
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 2px solid var(--accent);
  border-radius: 0;
  padding: 0 0 3px;
  font: inherit;
  font-size: var(--text-sm);
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
}
.link--muted{
  border-bottom-color: var(--rule);
  color: var(--muted);
}
.link:hover{ color: var(--accent); }
.link:focus-visible{
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
/* A finger needs ~44px, and the links are 25px tall: set as quiet small caps
   on purpose, they are about half of what a thumb can hit reliably — the CV
   and the address being the two things this page exists to hand over. The
   target grows, the type does not: an invisible box around each one, on a
   coarse pointer only, so a mouse still presses exactly what it can see.
   The dialog's close glyph is the same story at 18px. */
@media (pointer: coarse){
  .link, .cv-close{ position: relative; }
  .link::after{
    content: '';
    position: absolute;
    inset: -8px -6px;
  }
  .cv-close::after{
    content: '';
    position: absolute;
    inset: -13px -13px -13px -1px;
  }
}

/* --- Scene label -------------------------------------------------------- */

.scene-label{
  /* Sits directly over its world and is kept to the same width, so the label
     starts where the scene starts and the clock ends where it ends. stage.js
     publishes --stage-width once it knows the scale; before that (and if the
     scripts never run) the label just spans the column as any paragraph would. */
  width: 100%;
  /* ...but never squeezed narrower than the label itself needs: on a phone the
     world is only 240px wide and the label would wrap into a ragged stack, so
     below that floor it goes back to spanning its column. */
  max-width: max(22rem, var(--stage-width, 0px));
  margin: 0 auto;
  display: flex;
  /* Wrapping is not a narrow-screen nicety, it is what keeps the label from
     setting a floor under the width of everything around it: a nowrap flex row
     has a min-content width of ALL of its parts added up, and as a grid item
     that pushed the whole column — stage included — wider than the phone. */
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2);
  font-size: var(--text-xs);
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--muted);
}
.scene-index::after{ content: ' ·'; }
/* Each part of the label is one word-group and stays on one line. Flex wraps
   BETWEEN items; letting an item wrap inside itself is what produced the
   interleaved "01+02 OUT 0/6 FRI 19:00 / · TOGETHER ZURICH" mess on a phone,
   because two two-line items sitting side by side are read across, not down. */
.scene-label > *{ white-space: nowrap; }
.scene-clock{
  margin-left: auto;
  letter-spacing: 1px;
  /* The clock is the quietest thing on the page on purpose: it is evidence the
     scene is live, not a feature of it. */
  opacity: .75;
}

/* --- Stage -------------------------------------------------------------- */
/* No frame, no border, no vignette, no rounded corner. The scene is a flat
   rectangle of colour sitting directly on the page, which is what makes the
   pixel art read as art rather than as a screenshot of a game. */

.card-stage{
  position: relative;
  width: 100%;
  /* stage.js writes a pixel width onto the frame inside this box. Without
     min-width:0 that fixed child becomes the column's min-content, the column
     grows to it, the next measurement reads the wider column and the frame
     ratchets outwards — which is exactly the feedback loop the camera would
     otherwise ride all the way off the side of a phone. */
  min-width: 0;
}
.stage-frame{
  position: relative;
  overflow: hidden;
  /* stage.js sets the exact pixel width and height, and centres it with the
     auto margins below. */
  margin: 0 auto;
}

.stage{
  position: absolute;
  top: 0;
  left: 0;
  /* transform-origin matters as much as the integer scale: scaling from the
     centre would put the whole scene on a half pixel at odd sizes. */
  transform-origin: top left;
  overflow: hidden;
}

/* --- Scene backdrops ---------------------------------------------------- */
/* Two painted rooms, both authored in daylight. --horizon and --floor come
   from the scene data, so one backdrop covers several scenes. */

.stage--garden{
  --horizon: 46%;
  background:
    linear-gradient(180deg,
      var(--sky) 0%,
      #c3e2ee calc(var(--horizon) - 6%),
      var(--grass-dark) var(--horizon),
      var(--grass-dark) calc(var(--horizon) + 5%),
      var(--grass) calc(var(--horizon) + 5%),
      #a3cf72 100%);
}
/* A hedge line and a suggestion of rows, drawn rather than imaged. */
.stage--garden::before{
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(90deg,
      rgba(255,255,255,.05) 0 1px, rgba(255,255,255,0) 1px 8px),
    linear-gradient(180deg,
      rgba(0,0,0,0) calc(var(--horizon) + 5%),
      rgba(0,0,0,.05) calc(var(--horizon) + 5%));
}

.stage--desk{
  --floor: 64%;
  background:
    linear-gradient(180deg,
      var(--wall) 0%,
      var(--wall) calc(var(--floor) - 8%),
      var(--wall-band) calc(var(--floor) - 8%),
      var(--wall-band) var(--floor),
      var(--floorboard) var(--floor),
      #b98f5f 100%);
}
/* Floorboards, and a shaft of light across the wall. */
.stage--desk::before{
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(104deg,
      rgba(255,236,196,0) 44%, rgba(255,236,196,.35) 52%, rgba(255,236,196,0) 62%),
    repeating-linear-gradient(90deg,
      rgba(0,0,0,.05) 0 1px, rgba(0,0,0,0) 1px 10px);
}

/* The bar. The one scene both of them are in, so it belongs to neither
   palette: not her open green, not his warm indoor cream, but a dark room with
   two pools of light in it. Every colour it needs is declared here as a custom
   property, so the decor blocks below can pick them up without knowing which
   room they are in. */
.stage--bar{
  --floor: 66%;
  --bar-wall: #37292a;
  --bar-wall-dark: #2b2021;
  --bar-band: #47342f;
  --bar-floor: #6d4a30;
  --bar-floor-dark: #563a24;
  --bar-brass: #c79a4e;
  --bar-lit: #ffcf87;
  background:
    linear-gradient(180deg,
      var(--bar-wall-dark) 0%,
      var(--bar-wall) 20%,
      var(--bar-wall) calc(var(--floor) - 10%),
      var(--bar-band) calc(var(--floor) - 10%),
      var(--bar-band) var(--floor),
      var(--bar-floor) var(--floor),
      var(--bar-floor-dark) 100%);
}
/* Two pools of light — one over the bar, one over the booth — and floorboards.
   The pools are atmospheric so they may be soft gradients; the boards are an
   object, so they are a hard repeat on the pixel grid. Both characters stand
   in a pool, which is what keeps them legible under the night tint. */
.stage--bar::before{
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(ellipse 30% 46% at 22% 40%, rgba(255,198,116,.32), rgba(255,198,116,0) 70%),
    radial-gradient(ellipse 22% 42% at 73% 46%, rgba(255,178,98,.26), rgba(255,178,98,0) 72%),
    repeating-linear-gradient(90deg,
      rgba(0,0,0,.12) 0 1px, rgba(0,0,0,0) 1px 12px);
}

/* --- Time of day -------------------------------------------------------- */
/* A scene is painted once, in daylight, and tinted by the hour. The tint sits
   on .stage::after rather than inside the scene: .stage is transformed, so it
   forms its own stacking context and one overlay here lands above every prop,
   plant and character in it regardless of their z-index. */

.stage::after{
  content: '';
  position: absolute;
  inset: 0;
  z-index: 900;
  pointer-events: none;
  background-color: transparent;
  /* Long and linear: the tint changes when the hour rolls over while someone is
     looking at the page, and a snap would read as a glitch. */
  transition: background-color 900ms linear;
}
.stage.is-morning::after{ background-color: rgba(255, 222, 168, .22); }
.stage.is-day::after    { background-color: transparent; }
.stage.is-evening::after{ background-color: rgba(226, 133, 63, .22); }
.stage.is-night::after  { background-color: rgba(38, 42, 74, .46); }

/* --- Sprites ------------------------------------------------------------ */

.prop, .pixel-sprite, .avatar-sprite{
  image-rendering: pixelated; /* keep pixel art crisp when scaled up */
}
.prop{
  position: absolute;
  pointer-events: none;
}
.pixel-sprite{
  position: absolute;
  background-repeat: no-repeat;
  pointer-events: none;
}

/* --- CSS-only furniture ------------------------------------------------- */
/* Each of these is a rectangle placed by scenes.js in logical stage pixels and
   given a class; the stylesheet decides what it looks like. Flat blocks in the
   scene palette, so they sit with the sprites rather than under them. */

/* Never a pointer target. Decor is scenery and some of it is now an invisible
   collision volume covering most of the stage (.dk-wall), which would otherwise
   sit between the visitor and the thing they're trying to grab. */
.decor{ position: absolute; pointer-events: none; }

.gd-path{
  background: #c9a26f;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.22);
}
.dk-rug{
  background: #e8cfc2;
  box-shadow: inset 0 0 0 2px #d9b8a8;
}
.dk-skirting{ background: var(--wall-band); }
.dk-window{
  background: var(--sky);
  box-shadow: inset 0 0 0 2px #ffffff, 0 0 0 2px #e6dcc8;
}
/* Glazing bars, drawn rather than imaged. */
.dk-window::after{
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg, rgba(0,0,0,0) calc(50% - 1px), #fff calc(50% - 1px) calc(50% + 1px), rgba(0,0,0,0) calc(50% + 1px)),
    linear-gradient(180deg, rgba(0,0,0,0) calc(50% - 1px), #fff calc(50% - 1px) calc(50% + 1px), rgba(0,0,0,0) calc(50% + 1px));
}

/* --- Avatar ------------------------------------------------------------- */

.avatar{
  position: absolute;
  width: var(--avatar-box);
  height: var(--avatar-box);
  z-index: 300;
}
.avatar[tabindex]{ cursor: grab; }
.avatar[tabindex]:active{ cursor: grabbing; }
/* Without this a finger dragged on a character scrolls the page instead of
   moving them: the browser claims the gesture and sends pointercancel. It goes
   on the avatar and nowhere else — the rest of the scene must stay scrollable,
   because this is a CV page and reading it may never be blocked by decoration. */
.avatar{ touch-action: none; }

.avatar-sprite{
  width: 100%;
  height: 100%;
  background-repeat: no-repeat;
}
.avatar.facing-left .avatar-sprite{ transform: scaleX(-1); }

/* A mouse presses exactly what it can see. The cell is wider than the figure
   in it, and a click on the empty margin beside her is a click on the barrow
   behind it — not a hello, and in the bar not a grab of whoever happens to be
   standing next to the jukebox. avatar.js sets --figure-clip from the
   character's portrait box; the clip mirrors with the sprite. A finger keeps
   the whole cell to grab by, and input.js sorts out what it meant — so this is
   everything except the touch-primary query the prompt and the hint use. */
@media not all and (hover: none) and (pointer: coarse){
  .avatar{ pointer-events: none; }
  .avatar-sprite{ pointer-events: auto; clip-path: var(--figure-clip, none); }
  /* The portrait box is the STANDING figure. A pose or a stance fills the
     margin it was cutting off — the bike's wheels, the length of her in bed,
     arms out in warrior — and clipping to it cut the art itself. While posed
     the whole cell is hers, which is also what you'd want to click. */
  .avatar.posed .avatar-sprite,
  .avatar.in-stance .avatar-sprite{ clip-path: none; }
}

/* Selection is a little caret over her head, in her accent colour — not a ring
   round the cell: a box round a 32px square read as a selection rectangle out
   of a drawing program, and it was the biggest thing in the room. The caret is
   five whole logical pixels wide, built from 1px squares (box-shadows, no
   blur) so the stage scale keeps every one of them square; a CSS triangle
   would be scaled into a smooth vector diagonal. Its shape, not only its
   colour, is what says "this one". It sits on the avatar, not the sprite, so
   it never mirrors, and it bobs with her. */
.avatar:focus{ outline: none; }
.avatar.selected::before{
  content: '';
  position: absolute;
  /* The tip: centre column of the cell, clear of the top of her hair. Outlined
     in ink like the sprites are, so it holds against a busy room. */
  left: calc(50% - 1px);
  top: -1px;
  width: 1px;
  height: 1px;
  background: var(--accent);
  box-shadow:
    -2px -2px var(--accent), -1px -2px var(--accent), 0 -2px var(--accent),
    1px -2px var(--accent), 2px -2px var(--accent), -1px -1px var(--accent),
    0 -1px var(--accent), 1px -1px var(--accent), -2px -3px var(--ink),
    -1px -3px var(--ink), 0 -3px var(--ink), 1px -3px var(--ink),
    2px -3px var(--ink), -3px -2px var(--ink), 3px -2px var(--ink),
    -2px -1px var(--ink), 2px -1px var(--ink), -1px 0 var(--ink), 1px 0 var(--ink),
    0 1px var(--ink);
  pointer-events: none;
}

/* The idle bob is what stops a standing character reading as a broken image.
   Whole pixels only — a fractional translate would soften the sprite. */
.avatar:not(.moving){ animation: idle-bob 2.8s steps(1, end) infinite; }
@keyframes idle-bob{
  0%, 45%   { transform: translateY(0); }
  50%, 95%  { transform: translateY(-1px); }
  100%      { transform: translateY(0); }
}

/* The acknowledging hop: two little two-pixel hops, whole logical pixels on
   steps() timing. It used to be a 6-degree rotation, which resampled the sprite
   on every wave and left it visibly jagged for the length of the bubble — the
   one rotation on the page, on the one thing the visitor is looking at. */
.avatar.waving .avatar-sprite{ animation: wave .4s steps(1, end) 2; }
@keyframes wave{
  0%, 49%   { transform: translateY(-2px); }
  50%, 100% { transform: translateY(0); }
}
.avatar.facing-left.waving .avatar-sprite{ animation: wave-flipped .4s steps(1, end) 2; }
@keyframes wave-flipped{
  0%, 49%   { transform: scaleX(-1) translateY(-2px); }
  50%, 100% { transform: scaleX(-1) translateY(0); }
}

/* The speech bubble. Square, hard-edged, no tail — a rounded bubble next to
   pixel art always looks like it came from a different page. Sized like the
   proximity prompt below, and for the same reason: it is inside the stage, so
   every number here is a LOGICAL pixel the stage scale multiplies. It used to
   take the page's 10px label size and a 2px border, which at 4x is 40px text
   in an 8px frame — a placard, not a line someone says. */
.bubble{
  position: absolute;
  left: 50%;
  bottom: 100%;
  /* --bubble-shift is set by avatar.js, in whole logical pixels, to keep a
     bubble near either end of the room inside the stage instead of clipped. */
  transform: translate(calc(-50% + var(--bubble-shift, 0px)), -2px);
  padding: 1px 2px;
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--ink);
  font-size: 5px;
  line-height: 1.2;
  letter-spacing: 0;
  white-space: nowrap;
  z-index: 400;
}
.bubble[hidden]{ display: none; }

/* --- Footer ------------------------------------------------------------- */

.site-footer{
  display: flex;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
  font-size: var(--text-xs);
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--muted);
}

/* The controls hint, which has to be true on both kinds of device: a phone has
   no arrow keys and no E, and telling someone to press one is worse than
   saying nothing. Only the words that actually differ exist twice — the rest
   of the sentence is written once and shared — and which pair shows is a
   capability query, never a user-agent sniff. `(hover: none) and (pointer:
   coarse)` is the same test interact.js uses to name the verb in the prompt,
   so the footer and the prompt can never disagree. */
.hint-touch{ display: none; }
@media (hover: none) and (pointer: coarse){
  .hint-pointer{ display: none; }
  .hint-touch{ display: inline; }
}

/* --- CV dialog ---------------------------------------------------------- */

.cv-dialog{
  width: min(560px, calc(100vw - 2 * var(--sp-4)));
  max-height: 84vh;
  /* On a phone the large viewport is what vh means, so 84vh could run under
     the browser's own toolbar; dvh is the window actually showing. */
  max-height: 84dvh;
  overflow: auto;
  padding: var(--sp-5);
  border: 2px solid var(--ink);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-sm);
  letter-spacing: normal;
}
.cv-dialog::backdrop{ background: rgba(47, 43, 46, .38); }
/* Used when the browser has no <dialog> support: same panel, positioned by
   hand instead of by the platform. */
.cv-dialog.is-fallback{
  display: block;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 100;
  box-shadow: 0 0 0 100vmax rgba(47, 43, 46, .38);
}

.cv-close{
  float: right;
  appearance: none;
  background: none;
  border: 0;
  padding: 0 0 0 var(--sp-3);
  font: inherit;
  font-size: 18px;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
}
.cv-close:hover{ color: var(--ink); }
/* Same square ring as the links, in place of the rounded blue one WebKit
   draws on the button the dialog focuses when it opens. */
.cv-close:focus-visible{ outline: 2px solid var(--ink); outline-offset: 2px; }

.cv-badge{
  display: inline-block;
  margin-bottom: var(--sp-2);
  padding: 2px 6px;
  background: var(--wall-band);
  font-size: var(--text-xs);
  letter-spacing: 1px;
  text-transform: uppercase;
}
.cv-badge[hidden]{ display: none; }

.cv-head h2{ font-size: var(--text-lg); font-weight: 600; letter-spacing: -0.01em; line-height: 1.2; }
.cv-headline{ font-size: var(--text-xs); letter-spacing: 1px; text-transform: uppercase; color: var(--muted); }
.cv-bio{ margin-top: var(--sp-3); }

.cv-contact{
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-1) var(--sp-3);
  margin: var(--sp-3) 0 0;
  font-size: var(--text-xs);
}
.cv-contact dt{ letter-spacing: 1px; text-transform: uppercase; color: var(--muted); }
.cv-contact dd{ margin: 0; }
.cv-contact a{ color: var(--ink); }

.cv-note{ margin-top: var(--sp-2); font-size: var(--text-xs); color: var(--muted); }

.cv-body{ margin-top: var(--sp-5); }
.cv-section + .cv-section{ margin-top: var(--sp-5); }
.cv-section h3{
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 2px;
  color: var(--muted);
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--rule);
}
.cv-entry{ padding-top: var(--sp-3); }
.cv-when{ font-size: var(--text-xs); color: var(--muted); }
.cv-what{ font-size: var(--text-sm); }
.cv-where{ font-size: var(--text-xs); color: var(--muted); }
.cv-notes{ margin-top: var(--sp-1); }

.cv-dismiss{ margin-top: var(--sp-5); text-align: right; }

/* --- Narrow viewports --------------------------------------------------- */

/* Below this there isn't room for a world and a column of text beside it, so
   the diagonal is dropped: one column, each name and its links above its own
   world. The cap on the text column is what sets this width — a world squeezed
   any narrower stops being worth looking at. */
@media (max-width: 60rem){
  .world,
  .world--right{ grid-template-columns: minmax(0, 1fr); }
  .world > .world-meta,
  .world--right > .world-meta{ grid-column: 1; grid-row: 1; }
  .world > .card-stage,
  .world--right > .card-stage{ grid-column: 1; grid-row: 2; }

  /* Once the meta block sits ABOVE its world it is a heading for it, not a
     column beside it, and a whole-height portrait stacked on the name is ~95px
     of nothing much before the world starts. That matters most on a phone on
     its side, which is this width with only 320-380px of window: the portrait
     and the name alone filled the screen. So the portrait stands beside the
     name and links, at every width where the column is single, and the block
     is held to the width of the world under it — centred like the frame and
     its label, so the name starts where the world starts instead of hanging
     out at the page margin. Nothing is hidden and the order is the markup's.
     (The shared arrangement's strip is the same grid here: it set its own
     flex row, and its gap would otherwise spread the name off its links.) */
  .world-meta,
  body.is-together .world-meta{
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: var(--sp-4);
    row-gap: var(--sp-1);
    align-items: center;
    width: 100%;
    max-width: max(22rem, var(--stage-width, 0px));
    margin: 0 auto;
  }
  .world-meta .portrait{ grid-row: 1 / span 2; margin-bottom: 0; }
  /* With the portrait taking a column, CV and an email address may no longer
     fit on one line beside it. They wrap rather than overflow — an email
     address is one unbreakable word and a page that scrolls sideways is the
     one thing this layout may never do. The row gap is there for the finger:
     stacked, the two links' enlarged hit areas (below) would otherwise
     overlap by most of a line. */
  .person-links{ flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); }
  .world-meta .scene-label{
    grid-column: 1 / -1;
    margin-top: var(--sp-2);
    /* The block it is in is already the world's width. */
    max-width: none;
  }
}


@media (max-width: 34rem){
  body{ padding: var(--sp-5) var(--sp-4); gap: var(--sp-4); }
  /* Neither scene fits across a phone at a scale worth drawing — 240 logical
     pixels only fit at 1x and the bar's 336 not even that — so the frame
     becomes a window and the scene pans inside it to follow whoever is being
     steered. See the camera notes in stage.js; nothing here has to know, but
     it is why a stage is wider than its frame at this width and why the page
     still never scrolls sideways. */
  /* `flex: 0 1 auto` does nothing on a wide page, where the content is always
     taller than the window. On a phone in the shared arrangement it is the
     difference between the footer sitting under the last email address and
     being shoved to the bottom of the screen with a hole above it: let the
     leftover space fall after the footer rather than inside the page. */
  main{ gap: var(--sp-4); flex: 0 1 auto; }
  .person-name{ font-size: 22px; }
  .worlds{ gap: var(--sp-5); }

  /* Pushed to its own end of the row, the clock is what forces the label to
     wrap raggedly. In one narrow column it just follows the tally. */
  .scene-label .scene-clock{ margin-left: 0; }
  .scene-label{ row-gap: 0; }

  /* A 26px inset on each side of a 284px dialog leaves the CV a 230px column
     to be read in. The panel's own border already does the framing. */
  .cv-dialog{ padding: var(--sp-4); }
}

@media (prefers-reduced-motion: reduce){
  /* Not a nicety on this page: both characters idle-bob continuously, all day,
     whether or not anyone is interacting with them. Someone who has asked for
     less motion has asked for exactly this to stop. */
  .avatar:not(.moving){ animation: none; }
  .avatar.waving .avatar-sprite,
  .avatar.facing-left.waving .avatar-sprite{ animation: none; }
  /* The portraits stop completely, on frame 0 — a whole standing frame, not a
     slow loop. Only the name is dropped: the timing longhands are set inline
     from the character grid and an inline `animation` shorthand here could not
     be overridden. With no animation, background-position-x falls back to its
     initial 0 and the first cell of the row is what shows. */
  .portrait-sheet{ animation-name: none; }
  .stage::after{ transition: none; }
  .skip-link{ transition: none; }
}

/* Plants sit on the ground, so they belong behind anyone walking past. */
.plant{ z-index: 100; }

/* --- Furnishing the scenes ---------------------------------------------- *
 * Everything below is a .decor rectangle placed by scenes.js and given a
 * class. The furniture itself is sprites now (original art drawn by
 * tools/pixelart/*.py, see STYLE.md there); what stays here is what isn't an
 * object — walls, windows, ground, paths and pools of light — because that is
 * atmosphere, not art. It also scales with the stage for free, because it is
 * all blocks and gradients.
 *
 * House style, so a new piece fits without thinking about it: flat fills from
 * the scene palette, a 1-2px lighter inset along the top edge and a darker one
 * along the bottom to fake a light from above, and no rounded corners or
 * blur anywhere. Two inset shadows is usually the whole recipe.
 */

/* Garden ------------------------------------------------------------------ */
/* The garden's and the street's objects are sprites now (original art from
   tools/pixelart/outdoor.py). What stays here is ground and light. */

/* The pool of lantern light round the evening bench. Light, not an object,
   so it may be a soft gradient; it brightens as the evening deepens. */
.gd-glow{
  background: radial-gradient(closest-side, rgba(255,214,140,.38), rgba(255,214,140,0));
  opacity: .6;
}
.stage.is-evening .gd-glow{ opacity: .9; }
.stage.is-night .gd-glow{ opacity: 1; }
.gd-kerb{
  background: #c0b6a2;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.3);
}
.dk-mug{
  background: #e8e2d6;
  box-shadow: inset 0 -2px 0 #cfc6b4, inset 0 0 0 1px #b9b0a0;
}

/* Indoors ---------------------------------------------------------------- */

/* No paint at all. This one exists purely as the room's back wall for
   collision — the backdrop gradient already draws the wall itself, and giving
   this a fill would just cover it. Kept in the data as a rectangle so the
   walkable area of a room is visible where the rest of the furniture is. */
.dk-wall{ background: none; }

/* The furniture indoors is sprites now (tools/pixelart/indoor.py). What stays
   here is what is not an object: walls, floors, light and steam. A puff is an
   empty 2x2 box parked just above a sprite mug, kettle or pot, because the
   steam is drawn by .amb-steam::before and an <img> has no ::before. */
.dk-puff{ background: none; }
.dk-puff--quick.amb-steam::before{ animation-duration: 3.1s; }
/* The floor lamp's pool: light on the wall above it and the floor below. */
.dk-glow{
  background: radial-gradient(ellipse 50% 50% at 50% 30%,
    rgba(255,223,158,.42), rgba(255,223,158,0) 70%);
}

.dk-tiles{
  background:
    repeating-linear-gradient(90deg, #eae2d0 0 11px, #ded4bf 11px 12px),
    repeating-linear-gradient(180deg, #eae2d0 0 11px, #ded4bf 11px 12px);
}
.dk-cushion{
  background: #d8a48c;
  box-shadow: inset 0 2px 0 #e5b79f, inset 0 -2px 0 #bd8a72;
}
.dk-blind{
  background: repeating-linear-gradient(180deg, #efe7d8 0 2px, #ddd3c0 2px 4px);
  box-shadow: 0 1px 0 rgba(0,0,0,.12);
}

/* The bar ---------------------------------------------------------------- */
/* Every object in the bar is now a sprite (assets/props/bar/, drawn by
   tools/pixelart/rooms.py). What is left here is what is not an object: the
   collision wall, the skirting, the footrail, the mat, and the light. Colours
   come from the custom properties on .stage--bar, which these inherit as its
   descendants. */

/* No paint: this is the room's back wall for collision only, exactly like
   .dk-wall. The backdrop gradient already draws the wall itself. */
.bar-wall{ background: none; }

.bar-skirting{ background: #2b2021; }

/* The brass footrail along the base of the counter. */
.bar-rail{
  background: linear-gradient(180deg, var(--bar-brass) 0 2px, #8a6a34 2px 3px);
}

/* The mat under the booth. Dark, unlike the pale rugs indoors: a light one in
   here would be the brightest thing in the room. */
.bar-mat{
  background: #593a26;
  box-shadow: inset 0 2px 0 rgba(0,0,0,.22);
}

/* Glow round the lanterns, the sign and the jukebox. These sit ABOVE the
   hour's tint (z 950 in the scene data, over .stage::after at 900) and add
   light with `screen`, so a lit thing stays lit at night instead of being
   dimmed with the wall behind it. Soft gradients, because light is
   atmosphere; nothing here has an edge. */
.bar-glow, .rm-glow{ mix-blend-mode: screen; }
.bar-glow--red { background: radial-gradient(closest-side, rgba(255,128,92,.34), rgba(255,128,92,0)); }
.bar-glow--warm{ background: radial-gradient(closest-side, rgba(255,214,140,.34), rgba(255,214,140,0)); }
.bar-glow--neon{
  background:
    radial-gradient(closest-side at 32% 50%, rgba(95,240,255,.22), rgba(95,240,255,0)),
    radial-gradient(closest-side at 70% 50%, rgba(255,122,184,.26), rgba(255,122,184,0));
}
.bar-glow--juke{ background: radial-gradient(closest-side, rgba(255,190,120,.24), rgba(255,190,120,0)); }

/* Bedrooms at night ------------------------------------------------------ */
/* Same screen-blended light as the bar: the bedside lamp, the string lights
   over her bed and his monitor. */
.rm-glow--lamp  { background: radial-gradient(closest-side, rgba(255,210,130,.42), rgba(255,210,130,0)); }
.rm-glow--string{ background: radial-gradient(closest-side, rgba(255,200,150,.36), rgba(255,200,150,0)); }
.rm-glow--screen{ background: radial-gradient(closest-side, rgba(120,190,255,.34), rgba(120,190,255,0)); }
/* A puff of steam off a sprite mug: an empty box whose ::before is the puff
   (.amb-steam), because the mug is an <img> and has no pseudo-elements. */
.rm-steam{ background: none; }

/* --- Particle fields ---------------------------------------------------- *
 * Dust, fireflies and the odd butterfly. Each is one element with a repeating
 * radial-gradient for the specks and a slow background-position drift — many
 * particles for one box and no per-particle DOM node or JS.
 */

.gd-motes, .dk-motes{
  background-image:
    radial-gradient(circle, rgba(255,255,255,.55) 0 1px, rgba(255,255,255,0) 1px),
    radial-gradient(circle, rgba(255,255,255,.35) 0 1px, rgba(255,255,255,0) 1px);
  background-size: 57px 41px, 89px 53px;
  opacity: .7;
}
/* Dust only really shows in a shaft of light, so it leans on the warm hours. */
.stage.is-night .gd-motes,
.stage.is-night .dk-motes{ opacity: .25; }

.gd-flies{
  background-image:
    radial-gradient(circle, rgba(255,236,150,.9) 0 1px, rgba(255,236,150,0) 1px),
    radial-gradient(circle, rgba(255,236,150,.6) 0 1px, rgba(255,236,150,0) 1px);
  background-size: 67px 37px, 101px 59px;
  /* Fireflies are an evening and night thing; invisible the rest of the day. */
  opacity: 0;
  transition: opacity 900ms linear;
}
.stage.is-evening .gd-flies{ opacity: .75; }
.stage.is-night .gd-flies{ opacity: 1; }

.gd-butterfly{
  background-image:
    radial-gradient(circle, rgba(255,255,255,.85) 0 1px, rgba(255,255,255,0) 1px);
  background-size: 120px 47px;
  opacity: .8;
}
.stage.is-night .gd-butterfly{ opacity: 0; }

/* --- Ambient motion ----------------------------------------------------- *
 * All of it CSS keyframes, so the shared rAF loop stays responsible only for
 * things with state (the avatars, the cat, the proximity prompt) and this costs
 * no JavaScript per frame.
 *
 * Two constraints, and neither is optional.
 *
 * 1. NO ROTATION AND NO SUB-PIXEL MOVEMENT ON A SPRITE. Rotating pixel art by
 *    even half a degree resamples it and softens every edge, and a fractional
 *    translate does the same — which is the one thing this whole codebase is
 *    built to avoid (integer scales, `transform-origin: top left`, rounding at
 *    the point of drawing). So ambient movement is whole pixels on `steps()`
 *    timing, exactly like the avatars' idle-bob. It reads as pixel animation
 *    rather than as a smoothly sliding image, which is also just more correct
 *    for the medium.
 *
 * 2. A PROP is an <img> with an inline `transform: scale(n)` from placeProp, and
 *    a CSS animation on `transform` would override that inline value and drop
 *    the sprite back to 1x. Props therefore animate `opacity`, `filter`, or the
 *    independent `translate` property — which composes with `transform` rather
 *    than replacing it, and is applied in the parent's coordinate space, so
 *    `translate: 1px` is one logical stage pixel regardless of the sprite's own
 *    scale. Decor is a plain div and has no such restriction, but it uses the
 *    same integer-and-steps discipline so the whole page moves alike.
 */

.amb-drift{ animation: amb-drift 22s linear infinite; }
@keyframes amb-drift{
  from{ background-position: 0 0, 0 0; }
  to  { background-position: -57px -41px, 89px -53px; }
}

/* Trees and curtains: a one-pixel lean, held. At this scale that is a breeze;
   anything larger reads as the whole sprite sliding about. */
.amb-sway{ animation: amb-sway 7s steps(1, end) infinite; }
@keyframes amb-sway{
  0%, 49%  { translate: 0 0; }
  50%, 99% { translate: 1px 0; }
  100%     { translate: 0 0; }
}
.amb-curtain{ animation: amb-curtain 9s steps(1, end) infinite; }
@keyframes amb-curtain{
  0%, 45%  { translate: 0 0; }
  50%, 95% { translate: 1px 0; }
  100%     { translate: 0 0; }
}

/* Lamps and lanterns. Deliberately not a strobe: a slow breath with one
   shorter dip in it, so it reads as a filament rather than a fault. */
.amb-flicker{ animation: amb-flicker 5.5s ease-in-out infinite; }
@keyframes amb-flicker{
  0%, 100%{ filter: brightness(1); }
  38%     { filter: brightness(1.13); }
  44%     { filter: brightness(.97); }
  70%     { filter: brightness(1.06); }
}

/* A laptop screen, breathing very slightly. */
.amb-screen{ animation: amb-screen 4.5s ease-in-out infinite; }
@keyframes amb-screen{
  0%, 100%{ filter: brightness(1); }
  50%     { filter: brightness(1.09); }
}

/* Steam off a mug or a kettle: one puff, rising and fading, on a long loop so
   two mugs in one room don't pulse in time with each other. */
.amb-steam::before{
  content: '';
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: 2px;
  height: 2px;
  margin-left: -1px;
  background: rgba(255,255,255,.75);
  animation: amb-steam 4.2s steps(1, end) infinite;
}
/* One puff climbing in whole pixels and fading out. Stepped rather than eased
   for the same reason as everything else here: a 2px dot moving on fractions of
   a pixel is a grey smudge, not a dot. */
@keyframes amb-steam{
  0%  { opacity: 0;  transform: translateY(0); }
  15% { opacity: .8; transform: translateY(-2px); }
  40% { opacity: .7; transform: translateY(-4px); }
  65% { opacity: .5; transform: translateY(-6px); }
  85% { opacity: .25; transform: translateY(-8px); }
  100%{ opacity: 0;  transform: translateY(-8px); }
}

/* --- Kimchi ------------------------------------------------------------- *
 * Hannah's cat, drawn out of blocks. She dates from when the art could only
 * come from two licensed packs and neither had a cat; the furniture has since
 * become original sprites from tools/pixelart/, and she could follow it. Every
 * part is a named child div (see cat.js), so swapping the whole thing for a
 * spritesheet later means rewriting this section and nothing else.
 *
 * She is ginger, because a ginger cat is legible at sixteen pixels wide and a
 * tabby is not.
 */

.kimchi{
  position: absolute;
  width: var(--cat-w);
  height: var(--cat-h);
  /* Above the plants and the furniture, below the people. She is a cat: she
     gets to be underfoot, not in front of the person you came to look at. */
  z-index: 280;
  pointer-events: none;
}
.kimchi.facing-left{ transform: scaleX(-1); }

.kimchi > div{ position: absolute; }

.cat-body{
  left: 2px; bottom: 2px;
  width: 10px; height: 5px;
  background: #cf8347;
  box-shadow: inset 0 1px 0 #e09a5d, inset 0 -1px 0 #ad6635;
}
.cat-head{
  left: 9px; bottom: 5px;
  width: 6px; height: 5px;
  background: #d88d4f;
  box-shadow: inset 0 1px 0 #e8a468;
}
.cat-ear{
  position: absolute;
  top: -2px;
  width: 2px; height: 2px;
  background: #b97036;
}
.cat-ear--l{ left: 0; }
.cat-ear--r{ left: 4px; }
.cat-eye{
  position: absolute;
  top: 2px;
  width: 1px; height: 1px;
  background: #2f2b2e;
}
.cat-eye--l{ left: 1px; }
.cat-eye--r{ left: 4px; }
.cat-legs{
  left: 3px; bottom: 0;
  width: 9px; height: 2px;
  background:
    linear-gradient(90deg, #b97036 0 2px, rgba(0,0,0,0) 2px 5px, #b97036 5px 7px, rgba(0,0,0,0) 7px);
}
.cat-tail{
  left: 0; bottom: 4px;
  width: 4px; height: 2px;
  background: #cf8347;
  animation: cat-tail 3.6s steps(1, end) infinite;
}
/* A flick, in whole pixels — she is four pixels of tail, so a rotation would
   just anti-alias her into a smear. Long still stretch, then two quick beats,
   which is what a real idle tail does. */
@keyframes cat-tail{
  0%, 64%  { transform: translateY(0); }
  65%, 74% { transform: translateY(-1px); }
  75%, 84% { transform: translateY(-2px); }
  85%, 99% { transform: translateY(-1px); }
  100%     { transform: translateY(0); }
}

/* Walking: the legs alternate and the body takes a one-pixel bob. Steps, not a
   smooth ease — a smoothly bobbing pixel sprite looks like it is floating. */
.kimchi.is-walking .cat-legs{ animation: cat-step .32s steps(2, end) infinite; }
@keyframes cat-step{
  0%  { transform: translateX(0); }
  100%{ transform: translateX(2px); }
}
.kimchi.is-walking .cat-body{ animation: cat-bob .32s steps(2, end) infinite; }
@keyframes cat-bob{
  0%  { transform: translateY(0); }
  100%{ transform: translateY(-1px); }
}
/* A walking cat carries its tail up and still. */
.kimchi.is-walking .cat-tail{ animation: none; transform: translateY(-2px); }

/* Sitting: haunches down, tail curled round the front. */
.kimchi.is-sit .cat-body{ height: 4px; }
.kimchi.is-sit .cat-tail{ animation-duration: 5.2s; }

/* Grooming: the head dips towards the shoulder and back, on whole pixels. */
.kimchi.is-groom .cat-head{ animation: cat-groom 1.5s steps(1, end) infinite; }
@keyframes cat-groom{
  0%, 49%  { transform: translate(0, 0); }
  50%, 99% { transform: translate(-2px, 2px); }
  100%     { transform: translate(0, 0); }
}

/* Stretch: front end low, back end up, briefly longer than she really is. */
.kimchi.is-stretch .cat-body{ width: 12px; height: 4px; }
.kimchi.is-stretch .cat-head{ bottom: 3px; }

/* Asleep: a flat curl with the head tucked down and the eyes shut. The eyes
   become one-pixel lines rather than dots, which is the whole trick. */
.kimchi.is-sleep .cat-body{ height: 4px; width: 12px; left: 1px; }
.kimchi.is-sleep .cat-head{ bottom: 3px; left: 10px; }
.kimchi.is-sleep .cat-ear{ top: -1px; }
.kimchi.is-sleep .cat-eye{ width: 2px; height: 1px; background: #a06030; }
.kimchi.is-sleep .cat-legs{ display: none; }
.kimchi.is-sleep .cat-tail{ animation-duration: 7.5s; }

/* Her one bit of voice: a tiny mark over her head, rare by design. Not a
   .bubble — a bordered box at this size would be bigger than the cat. */
.cat-note{
  position: absolute;
  left: 50%;
  bottom: 100%;
  transform: translateX(-50%);
  font-size: 6px;
  line-height: 1;
  letter-spacing: 0;
  color: var(--ink);
  white-space: nowrap;
}
/* Counter the container's mirror so the text is never written backwards. */
.kimchi.facing-left .cat-note{ transform: translateX(-50%) scaleX(-1); }
.cat-note[hidden]{ display: none; }

/* --- Proximity prompt --------------------------------------------------- */
/* One per stage, moved to whatever is in reach. Deliberately quiet: it is a
   hint that something is usable, not a call to action. */

.prompt{
  position: absolute;
  transform: translate(-50%, -120%);
  padding: 0 2px;
  background: rgba(47, 43, 46, .72);
  color: var(--paper);
  /* Everything in here is a LOGICAL pixel: the prompt is inside the stage, so
     the integer stage scale multiplies it like it does the art. At 6px it was
     a caption sitting across a fifth of the room; 5px with the padding shaved
     is a tag on an object, which is what it is. It is legible from 2x up,
     which since the camera landed is the smallest scale a phone gets. */
  font-size: 5px;
  line-height: 1.2;
  letter-spacing: 0;
  white-space: nowrap;
  z-index: 500;
  pointer-events: none;
}
.prompt[hidden]{ display: none; }

/* A mouse over something usable. app.js sets the class from the same hit test
   a click uses, so the cursor never promises a click that does nothing. */
.stage.is-pointing{ cursor: pointer; }

/* The tally of what has been used in this scene. Sits with the scene label and
   is as quiet as the clock next to it — it is a record, not a score. */
.scene-found{
  letter-spacing: 1px;
  opacity: .6;
}

/* --- Reduced motion, part two ------------------------------------------- */
/* The block earlier in this file stops the avatars bobbing and waving. This one
   stops everything added since: the ambient scenery, the particle fields and
   every part of the cat. app.js separately declines to run the ambient scripts
   and the cat's state machine at all, so under `reduce` the page is a still
   life you can still walk around and use. Nothing here merely slows down. */

@media (prefers-reduced-motion: reduce){
  .amb-drift,
  .amb-sway,
  .amb-curtain,
  .amb-flicker,
  .amb-screen,
  .amb-steam::before,
  .cat-tail,
  .kimchi.is-walking .cat-legs,
  .kimchi.is-walking .cat-body,
  .kimchi.is-groom .cat-head{
    animation: none !important;
  }
  /* Steam with no animation would otherwise sit frozen at full opacity as a
     stray dot above every mug. */
  .amb-steam::before{ opacity: 0; }
  .gd-flies{ transition: none; }
}

/* --- A finger on the stage ------------------------------------------------ */
/* What a phone does to a press on a scene that a mouse never does, switched
   off on the stage frame and nowhere else:
     * double-tap to zoom. Tapping the floor twice to walk somewhere is a
       normal thing to do here, and on iOS it zoomed the page instead.
       `manipulation` keeps panning and pinch-zoom — the page still scrolls
       with a finger on the scene, which it must — and drops only that.
     * the long-press callout and text selection, which otherwise land on the
       bubble or the prompt the moment someone holds a character still.
     * the grey tap flash Android paints over a role=button, which here is a
       translucent square over the character.
   The avatar's own `touch-action: none` (above) still wins inside it: touch
   actions intersect down the tree. */
.stage-frame{
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

/* Where a tapped walk is headed. Five logical pixels by three — a mark on the
   floor, not a cursor — on the pixel grid like everything else, under the
   characters and over the floor. It exists only while she is on her way and
   never animates, so there is nothing for reduced motion to stop. */
.walk-target{
  z-index: 290;
  border: 1px solid rgba(255, 255, 255, .85);
  background: rgba(47, 43, 46, .25);
}

/* --- Work: her office, his office, the lab ------------------------------ */
/* Rooms declared in js/scenes-work.js. */

/* the painted backdrops (walls, floors, light) for the rooms in
 * js/scenes-work.js. Same rules as style.css: CSS is for what is not an object.
 *
 * Three rooms, three palette families, so nobody mistakes his office for hers:
 *   .stage--office-h  sage wall over a white wainscot, pale oak boards, sun
 *   .stage--office-m  cool blue-grey wall, grey carpet tiles, a strip light
 *   .stage--lab       white tiles over a mint band, grey vinyl, bright and flat
 * Each declares its colours as custom properties in its own block, and the
 * shared decor below (.wk-window, .wk-pool, ...) picks them up from there.
 *
 * Time of day is the usual .stage::after tint from style.css; the only thing
 * these rooms add per hour is what is outside the window and how strong the
 * light off it is. The furniture is sprites (tools/pixelart/work.py).
 */

/* --- Hannah's office ------------------------------------------------------ */
.stage--office-h{
  --floor: 68%;
  --wk-wall: #dfe9d3;
  --wk-wall-top: #d3e0c6;
  --wk-wainscot: #f4f1e6;
  --wk-rail: #c9d6b7;
  --wk-skirt: #b7c4a3;
  --wk-floor: #e1c497;
  --wk-floor-dark: #cfae7c;
  --wk-sky: #b9e1f2;
  --wk-sky-low: #e3f3f6;
  --wk-roof: #c98a6c;
  --wk-roof-dark: #a96f58;
  --wk-light: rgba(255, 244, 214, .5);
  background:
    linear-gradient(180deg,
      var(--wk-wall-top) 0%,
      var(--wk-wall) 14%,
      var(--wk-wall) calc(var(--floor) - 20%),
      var(--wk-rail) calc(var(--floor) - 20%),
      var(--wk-rail) calc(var(--floor) - 19%),
      var(--wk-wainscot) calc(var(--floor) - 19%),
      var(--wk-wainscot) calc(var(--floor) - 3%),
      var(--wk-skirt) calc(var(--floor) - 3%),
      var(--wk-skirt) var(--floor),
      var(--wk-floor) var(--floor),
      var(--wk-floor-dark) 100%);
}
/* Wainscot panel joints and floorboard joints: one hard 1px line every 16px,
   from the top of the wainscot down, so the panels and the boards line up. */
.stage--office-h::before{
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--floor) - 19%);
  bottom: 0;
  pointer-events: none;
  background: repeating-linear-gradient(90deg,
    rgba(150,120,80,.16) 0 1px, rgba(0,0,0,0) 1px 16px);
}

/* --- Martin's office ------------------------------------------------------ */
.stage--office-m{
  --floor: 68%;
  --wk-wall: #d6dde8;
  --wk-wall-top: #c9d1de;
  --wk-band: #b9c3d3;
  --wk-skirt: #8a94a8;
  --wk-floor: #7f8899;
  --wk-floor-dark: #6c7486;
  --wk-sky: #b5d3e6;
  --wk-sky-low: #dceaf2;
  --wk-roof: #9aa6b8;
  --wk-roof-dark: #7c889c;
  --wk-light: rgba(236, 244, 255, .28);
  background:
    linear-gradient(180deg,
      var(--wk-wall-top) 0%,
      var(--wk-wall) 10%,
      var(--wk-wall) calc(var(--floor) - 10%),
      var(--wk-band) calc(var(--floor) - 10%),
      var(--wk-band) calc(var(--floor) - 3%),
      var(--wk-skirt) calc(var(--floor) - 3%),
      var(--wk-skirt) var(--floor),
      var(--wk-floor) var(--floor),
      var(--wk-floor-dark) 100%);
}
/* Carpet tiles: a 12px grid, hard 1px seams, only below the floor line. */
.stage--office-m::before{
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: var(--floor);
  bottom: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(90deg, rgba(0,0,0,.09) 0 1px, rgba(0,0,0,0) 1px 12px),
    repeating-linear-gradient(180deg, rgba(0,0,0,.09) 0 1px, rgba(0,0,0,0) 1px 12px);
}

/* --- The lab -------------------------------------------------------------- */
.stage--lab{
  --floor: 68%;
  --wk-tile: #f4f7f6;
  --wk-grout: #e8eeec;
  --wk-mint: #c6e6d6;
  --wk-mint-dark: #a7d2bd;
  --wk-floor: #c4cecb;
  --wk-floor-dark: #b1bcb9;
  background:
    linear-gradient(180deg,
      var(--wk-tile) 0%,
      var(--wk-tile) calc(var(--floor) - 14%),
      var(--wk-mint) calc(var(--floor) - 14%),
      var(--wk-mint) calc(var(--floor) - 2%),
      var(--wk-mint-dark) calc(var(--floor) - 2%),
      var(--wk-mint-dark) var(--floor),
      var(--wk-floor) var(--floor),
      var(--wk-floor-dark) 100%);
}
/* Wall tiles on a 6px grid, above the mint band only. */
.stage--lab::before{
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: calc(var(--floor) - 14%);
  pointer-events: none;
  background:
    repeating-linear-gradient(90deg, var(--wk-grout) 0 1px, rgba(0,0,0,0) 1px 6px),
    repeating-linear-gradient(180deg, var(--wk-grout) 0 1px, rgba(0,0,0,0) 1px 6px);
}

/* --- Shared decor --------------------------------------------------------- */

/* A window: sky by the hour, and a line of roofs across the bottom of it in
   hard steps, so the next street is on the pixel grid rather than a blur. */
.wk-window{
  --wk-hill: #b7cfae;
  background: linear-gradient(180deg, var(--wk-sky) 0%, var(--wk-sky-low) 70%);
  box-shadow: inset 0 0 0 2px #ffffff, 0 0 0 2px #e9e4d6;
}
/* The next street: roofs of different heights, each a hard block on the grid,
   in front of a band of far hill. Several plain blocks rather than one
   repeating pattern, which read as battlements. */
.wk-window::before{
  content: '';
  position: absolute;
  left: 2px;
  right: 2px;
  bottom: 2px;
  height: 44%;
  background:
    linear-gradient(var(--wk-roof), var(--wk-roof)) 0 100% / 13px 50% no-repeat,
    linear-gradient(var(--wk-roof-dark), var(--wk-roof-dark)) 13px 100% / 9px 72% no-repeat,
    linear-gradient(var(--wk-roof), var(--wk-roof)) 22px 100% / 12px 38% no-repeat,
    linear-gradient(var(--wk-roof-dark), var(--wk-roof-dark)) 34px 100% / 7px 60% no-repeat,
    linear-gradient(var(--wk-roof), var(--wk-roof)) 41px 100% / 12px 46% no-repeat,
    linear-gradient(var(--wk-hill), var(--wk-hill)) 0 100% / 100% 84% no-repeat;
}
/* Glazing bars, as .dk-window draws them. */
.wk-window::after{
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg, rgba(0,0,0,0) calc(50% - 1px), #fff calc(50% - 1px) calc(50% + 1px), rgba(0,0,0,0) calc(50% + 1px)),
    linear-gradient(180deg, rgba(0,0,0,0) calc(40% - 1px), #fff calc(40% - 1px) calc(40% + 1px), rgba(0,0,0,0) calc(40% + 1px));
}
.wk-window--m{ --wk-hill: #b3c0cc; box-shadow: inset 0 0 0 2px #eef1f6, 0 0 0 2px #a9b2c3; }
.wk-window--m::after{
  background:
    linear-gradient(90deg, rgba(0,0,0,0) calc(50% - 1px), #eef1f6 calc(50% - 1px) calc(50% + 1px), rgba(0,0,0,0) calc(50% + 1px));
}

/* Blinds over his window: slats on a 3px pitch, pulled two-thirds down. */
.wk-blinds{
  background: repeating-linear-gradient(180deg, #eef1f6 0 2px, #c3cad6 2px 3px);
  -webkit-mask: linear-gradient(180deg, #000 0 64%, rgba(0,0,0,0) 64%);
          mask: linear-gradient(180deg, #000 0 64%, rgba(0,0,0,0) 64%);
}

.wk-sill{
  background: #ffffff;
  box-shadow: inset 0 -1px 0 #d9d3c3;
}
/* Radiator fins under her window: 2px fins, 1px gaps. */
.wk-radiator{
  background: repeating-linear-gradient(90deg, #fbfbf7 0 2px, #d8d6cc 2px 3px);
  box-shadow: inset 0 1px 0 #ffffff, inset 0 -1px 0 #c6c3b7;
}

/* The light off a window, lying on the floor in front of it. Atmosphere, so a
   soft gradient is allowed. */
.wk-pool{
  background: radial-gradient(ellipse 50% 50% at 50% 30%, var(--wk-light), rgba(0,0,0,0) 72%);
}

/* Hazard tape on the lab floor in front of the fume hood: flat ground, on the
   grid, 2px stripes. */
.wk-tape{
  background: repeating-linear-gradient(90deg, #f2c14e 0 3px, #3e3f48 3px 5px);
}

/* A ceiling strip light: a hard bar, and its glow on the wall under it. */
.wk-strip{
  background: #ffffff;
  box-shadow: 0 1px 0 #c3cad6, 0 6px 14px rgba(255,255,255,.55);
}

/* The lit interior of the fume hood, added with `screen` above the tint so it
   stays the brightest box in the room at any hour. */
.wk-hood-glow{
  mix-blend-mode: screen;
  background: radial-gradient(closest-side, rgba(255,250,214,.42), rgba(255,250,214,0));
}

/* The centrifuge's run light. */
.wk-led{ background: #9ad66b; }
.amb-blink{ animation: wk-blink 2.4s steps(1, end) infinite; }
@keyframes wk-blink{
  0%, 59%  { opacity: 1; }
  60%, 100%{ opacity: .15; }
}

/* --- The hour, through the window ------------------------------------------ */
/* Offices are a daytime thing, but a scene can be put on at any hour, so the
   sky outside still keeps time: warm at the start, amber at the end, dark with
   the roofs gone to silhouettes at night, when the light off it goes too. */
.stage.is-morning .wk-window{ --wk-sky: #f6dcc0; --wk-sky-low: #fdf0dc; }
.stage.is-evening .wk-window{ --wk-sky: #f0a46e; --wk-sky-low: #f8d3a1; }
.stage.is-night   .wk-window{
  --wk-sky: #232a4c; --wk-sky-low: #36406a; --wk-hill: #2a3352;
  --wk-roof: #2b2f45; --wk-roof-dark: #1f2236;
}
.stage.is-evening .wk-pool{ opacity: .6; }
.stage.is-night   .wk-pool{ opacity: 0; }

/* --- Reduced motion --------------------------------------------------------- */
/* The one keyframe this file adds. Stopped, the light stays on. */
@media (prefers-reduced-motion: reduce){
  .amb-blink{ animation: none !important; opacity: 1; }
}

/* --- Hannah's evenings at home, and her night --------------------------- */
/* Rooms declared in js/scenes-hannah-home.js. */

/* the painted backdrops (walls, floors, light) for the rooms in
 * js/scenes-hannah-home.js. Same rules as style.css: CSS is for what is not an object.
 *
 * .stage--hh is her home: one backdrop recipe (wall, a band where wall meets
 * floor, floorboards) whose colours are custom properties, and one class per
 * room that sets them: .stage--living (sage wallpaper), .stage--h-kitchen
 * (cream wall, terracotta floor tiles), .stage--h-bedroom (dusk-blue wall).
 * The hour's tint still comes from .stage.is-evening / .is-night in style.css;
 * the light that should survive it (lamps, the telly, the moon) is decor at
 * z 950, above the tint, blended with `screen`.
 */

.stage--hh{
  --floor: 66%;
  --hh-wall: #f1e6d2;
  --hh-wall-stripe: rgba(255, 255, 255, 0);
  --hh-band: #d9c3a0;
  --hh-floor: #c39a68;
  --hh-floor-dark: #a97f50;
  --hh-seam: rgba(70, 40, 20, .14);
  background:
    linear-gradient(180deg,
      rgba(0, 0, 0, 0) 0,
      rgba(0, 0, 0, 0) calc(var(--floor) - 3%),
      var(--hh-band) calc(var(--floor) - 3%),
      var(--hh-band) var(--floor),
      var(--hh-floor) var(--floor),
      var(--hh-floor-dark) 100%),
    repeating-linear-gradient(90deg,
      var(--hh-wall-stripe) 0 1px, rgba(255, 255, 255, 0) 1px 8px),
    var(--hh-wall);
}
/* The floor: boards (or tiles) on the pixel grid, starting at the floor line. */
.stage--hh::before{
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: var(--floor);
  bottom: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(180deg,
      rgba(0, 0, 0, 0) 0 11px, var(--hh-seam) 11px 12px),
    repeating-linear-gradient(90deg,
      var(--hh-seam) 0 1px, rgba(0, 0, 0, 0) 1px 24px);
}

/* The living room: sage wallpaper with a fine pinstripe, honey floorboards. */
.stage--living{
  --hh-wall: #cdd6b2;
  --hh-wall-stripe: rgba(255, 255, 255, .16);
  --hh-band: #a9b78f;
}
/* The kitchen: warm cream plaster, terracotta tiles on the floor. */
.stage--h-kitchen{
  --hh-wall: #f3e7cf;
  --hh-band: #d7c09a;
  --hh-floor: #c8794f;
  --hh-floor-dark: #b0623c;
  --hh-seam: rgba(80, 30, 10, .22);
}
.stage--h-kitchen::before{
  background:
    repeating-linear-gradient(180deg,
      rgba(0, 0, 0, 0) 0 7px, var(--hh-seam) 7px 8px),
    repeating-linear-gradient(90deg,
      rgba(0, 0, 0, 0) 0 11px, var(--hh-seam) 11px 12px),
    conic-gradient(rgba(255, 255, 255, .07) 25%, rgba(0, 0, 0, 0) 0 50%,
      rgba(255, 255, 255, .07) 0 75%, rgba(0, 0, 0, 0) 0) 0 0 / 24px 16px;
}
/* The bedroom: a dusk-blue wall and darker boards, already a night room. */
.stage--h-bedroom{
  --hh-wall: #aeb5cf;
  --hh-wall-stripe: rgba(255, 255, 255, .08);
  --hh-band: #8e93b0;
  --hh-floor: #a07a55;
  --hh-floor-dark: #85613f;
}

/* Skirting along the foot of the wall, and the kitchen's splashback tiles. */
.hh-skirting{
  background: #f6efe2;
  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .12);
}
.stage--h-bedroom .hh-skirting{ background: #c7cbe0; }
.hh-tiles{
  background:
    repeating-linear-gradient(90deg, rgba(0, 0, 0, 0) 0 7px, #c9d6c4 7px 8px),
    repeating-linear-gradient(180deg, rgba(0, 0, 0, 0) 0 7px, #c9d6c4 7px 8px),
    #e4ecdd;
}

/* --- Light ---------------------------------------------------------------- */
/* Above the hour's tint and added with `screen`, so a lit lamp stays lit at
   night instead of being dimmed with the wall behind it. Soft gradients,
   because light is atmosphere; nothing here has an edge. */
.hh-glow{ mix-blend-mode: screen; }
.hh-glow--lamp{
  background: radial-gradient(closest-side at 50% 42%,
    rgba(255, 206, 128, .40), rgba(255, 206, 128, 0));
}
.hh-glow--bright{
  background: radial-gradient(closest-side at 50% 42%,
    rgba(255, 214, 140, .52), rgba(255, 214, 140, 0));
}
.hh-glow--low{ opacity: .55; }
.hh-glow--fairy{
  background: radial-gradient(closest-side,
    rgba(255, 200, 150, .30), rgba(255, 200, 150, 0));
}
/* The telly: cool light across the floor and up the wall towards her chair. */
.hh-glow--tv{
  background: radial-gradient(closest-side at 45% 40%,
    rgba(130, 196, 255, .40), rgba(130, 196, 255, 0));
}
.hh-glow--pendant{
  background: radial-gradient(closest-side at 50% 40%,
    rgba(255, 214, 140, .44), rgba(255, 214, 140, 0));
}
.hh-glow--hob{
  background: radial-gradient(closest-side,
    rgba(255, 150, 90, .24), rgba(255, 150, 90, 0));
}
/* Moonlight through the bedroom window, falling down onto the floor. */
.hh-glow--moon{
  background:
    radial-gradient(closest-side at 50% 22%,
      rgba(170, 196, 255, .30), rgba(170, 196, 255, 0)),
    radial-gradient(ellipse 46% 16% at 46% 88%,
      rgba(170, 196, 255, .20), rgba(170, 196, 255, 0));
}
.hh-glow--night{
  background: radial-gradient(closest-side,
    rgba(255, 196, 150, .45), rgba(255, 196, 150, 0));
}

/* The telly's light changes with the picture: held levels, stepped, never a
   strobe. Opacity only, on decor. */
.hh-tv-flicker{ animation: hh-tv-flicker 9s steps(1, end) infinite; }
@keyframes hh-tv-flicker{
  0%   { opacity: 1; }
  22%  { opacity: .82; }
  41%  { opacity: .94; }
  58%  { opacity: .74; }
  77%  { opacity: 1; }
  88%  { opacity: .88; }
}

@media (prefers-reduced-motion: reduce){
  .hh-tv-flicker{ animation: none !important; }
}

/* --- Hannah's weekends out: yoga, the lake, the opera ------------------- */
/* Rooms declared in js/scenes-hannah-out.js. */

/* the painted backdrops (walls, floors, light) for the rooms in
 * js/scenes-hannah-out.js. Same rules as style.css: CSS is for what is not an object.
 *
 * The stage is a 240x136 box of logical pixels (stage.js scales the whole
 * thing by a whole number), so lengths here are logical px. Every scene still
 * takes the hour's tint from .stage::after in style.css; the glows that must
 * stay lit through it sit above it (z 950 in the scene data) and add light
 * with `screen`, exactly like the bar's.
 */

/* --- The yoga studio ---------------------------------------------------- */
/* Her palette family, indoors and early: warm plaster, a sage band, pale oak. */
.stage--yoga{
  --floor: 66%;
  --yg-wall: #f4ece0;
  --yg-wall-hi: #fbf6ee;
  --yg-band: #c9dcbf;
  --yg-floor: #e3c597;
  --yg-floor-dark: #cfa974;
  background:
    radial-gradient(ellipse 60% 70% at 18% 20%, rgba(255,244,214,.4), rgba(255,244,214,0) 70%),
    linear-gradient(180deg,
      var(--yg-wall-hi) 0%,
      var(--yg-wall) 30%,
      var(--yg-wall) calc(var(--floor) - 9%),
      var(--yg-band) calc(var(--floor) - 9%),
      var(--yg-band) var(--floor),
      var(--yg-floor) var(--floor),
      var(--yg-floor-dark) 100%);
}
/* Floorboards on the pixel grid, on the floor only. */
.stage--yoga::before{
  content: '';
  position: absolute;
  inset: 90px 0 0 0;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(0,0,0,.06) 0 1px, rgba(0,0,0,0) 1px),
    repeating-linear-gradient(90deg, rgba(0,0,0,0) 0 15px, rgba(120,80,40,.12) 15px 16px);
}
.yg-wall{ background: none; }
.yg-skirting{ background: #b7ccad; box-shadow: inset 0 1px 0 rgba(255,255,255,.4); }
/* A window's light landing on the floor in front of it. */
.yg-shaft{
  background: radial-gradient(ellipse 50% 100% at 50% 0, rgba(255,247,222,.6), rgba(255,247,222,0));
}
.stage.is-evening .yg-shaft{ opacity: .5; }
.stage.is-night .yg-shaft{ opacity: 0; }

/* --- Round the lake ----------------------------------------------------- */
/* Open and green, like the garden: sky over the Alps, the lake, the stone
   edge of the quay, and the gravel promenade she rides along. The Alps and
   the far shore are a sprite (assets/props/out/alps.png at y 24..56); the
   water starts where its shore ends. */
.stage--ride{
  --rd-sky-top: #8ccbf0;
  --rd-sky: #cdeaf8;
  --rd-lake: #6fb3d9;
  --rd-lake-deep: #4f93c2;
  --rd-path: #e3d6bb;
  --rd-path-dark: #d2c3a3;
  background:
    radial-gradient(circle at 22% 6%, rgba(255,250,220,.55), rgba(255,250,220,0) 30%),
    linear-gradient(180deg,
      var(--rd-sky-top) 0,
      var(--rd-sky) 50px,
      var(--rd-lake-deep) 54px,
      var(--rd-lake) 74px,
      #86c3e3 89px,
      var(--rd-path) 98px,
      var(--rd-path-dark) 136px);
}
/* The gravel's grain on the grid, on the path only. */
.stage--ride::before{
  content: '';
  position: absolute;
  inset: 98px 0 0 0;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(0,0,0,.08) 0 1px, rgba(0,0,0,0) 1px),
    repeating-linear-gradient(90deg, rgba(0,0,0,0) 0 6px, rgba(140,120,90,.10) 6px 7px);
}
.rd-wall{ background: none; }
/* Glints on the water: three sparse fields of whole-pixel dashes at
   different pitches, so they don't line up into columns. Each dash is a
   conic-gradient corner — a hard-edged w x 1 block at the tile's top left. */
.rd-glint{
  background-image:
    conic-gradient(from 270deg at 3px 1px, rgba(255,255,255,.8) 0 90deg, rgba(255,255,255,0) 90deg),
    conic-gradient(from 270deg at 2px 1px, rgba(255,255,255,.6) 0 90deg, rgba(255,255,255,0) 90deg),
    conic-gradient(from 270deg at 4px 1px, rgba(255,255,255,.45) 0 90deg, rgba(255,255,255,0) 90deg);
  background-size: 37px 13px, 53px 17px, 71px 23px;
  background-position: 0 2px, 19px 9px, 40px 5px;
  animation: rd-glint 6s steps(1, end) infinite;
}
@keyframes rd-glint{
  0%, 49%  { background-position: 0 2px, 19px 9px, 40px 5px; }
  50%, 100%{ background-position: 3px 2px, 16px 9px, 43px 5px; }
}
.rd-quay{
  background:
    repeating-linear-gradient(90deg, rgba(0,0,0,0) 0 15px, rgba(90,84,74,.35) 15px 16px),
    linear-gradient(180deg, #e9e3d6 0 2px, #c9c0ae 2px 8px, #a39a88 8px);
}
.rd-lane{ background: rgba(255,255,255,.7); }
/* Boats riding the swell: one pixel up and down, held. */
.rd-bob{ animation: rd-bob 3.6s steps(1, end) infinite; }
.rd-bob--slow{ animation-duration: 5.2s; }
@keyframes rd-bob{
  0%, 49%  { translate: 0 0; }
  50%, 100%{ translate: 0 1px; }
}

/* --- The opera ---------------------------------------------------------- */
/* Its own family again, like the bar: deep red silk walls, gilt, a red
   carpet. Everything the room's light needs is declared here. */
.stage--opera{
  --floor: 66%;
  --op-wall: #7a2632;
  --op-wall-dark: #5a1a25;
  --op-gold: #e2b35a;
  --op-carpet: #a3343c;
  --op-carpet-dark: #7b2530;
  background:
    linear-gradient(180deg,
      var(--op-wall-dark) 0%,
      var(--op-wall) 24%,
      var(--op-wall) var(--floor),
      var(--op-carpet) var(--floor),
      var(--op-carpet-dark) 100%);
}
/* Damask on the walls (a grid of darker diamonds' worth of dots, on the
   pixel grid) and a pile line in the carpet. */
.stage--opera::before{
  content: '';
  position: absolute;
  inset: 0 0 46px 0;
  pointer-events: none;
  background:
    radial-gradient(circle, rgba(40,6,14,.28) 0 1px, rgba(40,6,14,0) 1px) 0 0 / 8px 8px,
    radial-gradient(circle, rgba(255,190,150,.10) 0 1px, rgba(255,190,150,0) 1px) 4px 4px / 8px 8px;
}
.op-wall{ background: none; }
.op-dado{
  background: linear-gradient(180deg, var(--op-gold) 0 1px, #a9793a 1px 2px, #5a1a25 2px);
}
.op-skirting{ background: linear-gradient(180deg, var(--op-gold) 0 1px, #3c1018 1px); }
/* The way out to the foyer: a lit doorway in a gilt frame. */
.op-door{
  background: linear-gradient(180deg, #ffe6b0, #f2c27a);
  box-shadow: inset 0 0 0 2px var(--op-gold), inset 0 0 0 3px #7d5524;
}
.op-runner{
  background: #8a2a33;
  box-shadow: inset 0 1px 0 var(--op-gold), inset 0 -1px 0 var(--op-gold);
}
.op-glow{ mix-blend-mode: screen; }
.op-glow--chandelier{
  background: radial-gradient(closest-side, rgba(255,214,140,.42), rgba(255,214,140,0));
}
.op-glow--stage{
  background: radial-gradient(ellipse 50% 50% at 50% 70%, rgba(255,226,170,.30), rgba(255,226,170,0));
}
.op-glow--door{
  background: radial-gradient(closest-side, rgba(255,214,150,.32), rgba(255,214,150,0));
}

/* --- Reduced motion ----------------------------------------------------- */
/* The glints and the boats come to a full stop; the scene, the light and the
   tint stay. (amb-sway, amb-flicker, amb-drift and amb-steam are already
   stopped by style.css.) */
@media (prefers-reduced-motion: reduce){
  .rd-glint,
  .rd-bob{
    animation: none !important;
  }
}

/* --- Martin's evenings, weekends and late nights ------------------------ */
/* Rooms declared in js/scenes-martin.js. */

/* the painted backdrops (sky, grass, walls, floors, light)
 * for the places in js/scenes-martin.js: the pitch, the golf course, his
 * kitchen and the record room. Same rules as style.css: CSS is for what is
 * not an object. Every object is a sprite from tools/pixelart/martin_life.py.
 *
 * Units are logical stage pixels: .stage is 240x136 and scaled up whole, so
 * 1px here is one sprite pixel. Hard edges sit on that grid; anything soft
 * is light or distance, never an object.
 *
 * Each place declares its colours as custom properties on its stage class and
 * the hour (.is-morning / .is-day / .is-evening / .is-night, set by app.js)
 * only ever overrides those, or switches a light on.
 *
 * No keyframes are added here: the motion in these scenes is the shared
 * amb-* classes, which style.css already stops under reduced motion. The
 * reduce block at the bottom drops the fades on the lights as well.
 */

/* --- The pitch ----------------------------------------------------------- */

.stage--pitch{
  --pt-sky-top: #8ec5e0;
  --pt-sky: #d4ebf1;
  --pt-far: #7cae5a;
  --pt-grass: #73b052;
  --pt-grass-2: #69a64a;
  background:
    linear-gradient(180deg,
      var(--pt-sky-top) 0, var(--pt-sky) 40px,
      var(--pt-far) 40px, var(--pt-far) 77px,
      rgba(0,0,0,0) 77px),
    /* mowing stripes, on the pixel grid */
    repeating-linear-gradient(90deg,
      var(--pt-grass) 0 20px, var(--pt-grass-2) 20px 40px);
}
/* Training is Monday at seven: the sky has gone by then, which is what the
   lights are for. */
.stage--pitch.is-evening{
  --pt-sky-top: #2f3a63;
  --pt-sky: #8a6f8a;
}
.stage--pitch.is-night{
  --pt-sky-top: #161c36;
  --pt-sky: #2c3558;
}

.pt-back{ background: none; }

/* A tree line in the distance: soft, because it is far away. */
.pt-trees{
  background:
    radial-gradient(ellipse 11px 20px at 50% 100%, #5f9a55 0 80%, rgba(95,154,85,0) 100%) 11px 6px / 26px 26px repeat-x,
    radial-gradient(ellipse 12px 24px at 50% 100%, #4f8a4a 0 80%, rgba(79,138,74,0) 100%) 0 2px / 22px 30px repeat-x,
    linear-gradient(180deg, rgba(0,0,0,0) 24px, #4f8a4a 24px);
}

/* The ball-stop fence along the far side: posts, a top rail, a mesh you can
   see the trees through, and a strip of boards at its foot. */
.pt-fence{
  background:
    repeating-linear-gradient(90deg, #e9e4d6 0 30px, #3f5277 30px 60px, #b8403e 60px 90px)
      0 100% / 100% 5px no-repeat,
    repeating-linear-gradient(90deg, #355b42 0 2px, rgba(0,0,0,0) 2px 40px),
    linear-gradient(180deg, #355b42 0 2px, rgba(0,0,0,0) 2px),
    repeating-linear-gradient(90deg, rgba(40,70,50,.26) 0 1px, rgba(0,0,0,0) 1px 3px),
    repeating-linear-gradient(180deg, rgba(40,70,50,.26) 0 1px, rgba(0,0,0,0) 1px 3px);
}

.pt-line{ background: rgba(255,255,255,.82); }

/* The floodlights. Above the hour's tint (z 950 in the data) and added with
   `screen`, so they stay lit; off in daylight, on from the evening. */
.pt-flood, .pt-pool{
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity 900ms linear;
}
.pt-flood{
  background: radial-gradient(closest-side, rgba(255,250,226,.95), rgba(255,250,226,0));
}
.pt-pool{
  background:
    radial-gradient(ellipse 40% 70% at 12% 40%, rgba(232,244,255,.30), rgba(232,244,255,0) 80%),
    radial-gradient(ellipse 40% 70% at 88% 40%, rgba(232,244,255,.30), rgba(232,244,255,0) 80%);
}
.stage--pitch.is-evening .pt-flood,
.stage--pitch.is-night .pt-flood,
.stage--pitch.is-evening .pt-pool,
.stage--pitch.is-night .pt-pool{ opacity: 1; }

/* --- The golf course ----------------------------------------------------- */

.stage--golf{
  --gf-sky-top: #9fd0ea;
  --gf-sky: #e4f3f4;
  --gf-rough: #79aa55;
  --gf-fair: #94d266;
  --gf-fair-2: #8ac85d;
  background:
    linear-gradient(180deg,
      var(--gf-sky-top) 0, var(--gf-sky) 34px,
      var(--gf-rough) 34px, var(--gf-rough) 60px,
      rgba(0,0,0,0) 60px),
    /* the rough again along the near edge */
    linear-gradient(180deg, rgba(0,0,0,0) 128px, var(--gf-rough) 128px),
    repeating-linear-gradient(90deg,
      var(--gf-fair) 0 24px, var(--gf-fair-2) 24px 48px);
}
.stage--golf.is-morning{
  --gf-sky-top: #b9d8ea;
  --gf-sky: #fbefd9;
}

.gf-back{ background: none; }

/* Hills beyond the course: far away, so soft. */
.gf-hills{
  background:
    radial-gradient(ellipse 80px 20px at 50px 30px, #a8cfa2, rgba(168,207,162,0) 72%),
    radial-gradient(ellipse 100px 22px at 180px 32px, #9cc597, rgba(156,197,151,0) 72%);
}
.gf-trees{
  background:
    radial-gradient(circle at 50% 100%, #3f7a43 0 7px, rgba(63,122,67,0) 10px) 0 0 / 18px 16px repeat-x,
    radial-gradient(circle at 50% 100%, #4f8a4a 0 6px, rgba(79,138,74,0) 9px) 9px 3px / 21px 13px repeat-x,
    linear-gradient(180deg, rgba(0,0,0,0) 12px, #3f7a43 12px);
}

/* The tee box: mown shorter, so lighter, with a hard edge on the grid. */
.gf-tee{
  background: #a8dd7a;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.28), inset 0 -1px 0 rgba(0,0,0,.08);
}

/* Early haze over the far end. Only in the morning. */
.gf-mist{
  background: linear-gradient(180deg,
    rgba(255,255,255,0), rgba(255,255,255,.38) 50%, rgba(255,255,255,0));
  opacity: 0;
  transition: opacity 900ms linear;
}
.stage--golf.is-morning .gf-mist{ opacity: 1; }

/* --- His kitchen --------------------------------------------------------- */
/* A pale sage wall and a terracotta tiled floor: his, not her cream-and-wood
   kitchen. The window's afternoon light falls across the wall. */

.stage--m-kitchen{
  --floor: 68%;
  --mk-wall: #e6e4cc;
  --mk-band: #d3cfb2;
  --mk-floor: #c98a62;
  --mk-floor-dark: #b27552;
  --wall-band: var(--mk-band);
  background:
    linear-gradient(104deg,
      rgba(255,240,205,0) 30%, rgba(255,240,205,.32) 38%, rgba(255,240,205,0) 48%),
    linear-gradient(180deg,
      var(--mk-wall) 0%,
      var(--mk-wall) calc(var(--floor) - 6%),
      var(--mk-band) calc(var(--floor) - 6%),
      var(--mk-band) var(--floor),
      var(--mk-floor) var(--floor),
      var(--mk-floor-dark) 100%);
}
/* Floor tiles: grout lines on the grid, on the floor only. */
.stage--m-kitchen::before{
  content: '';
  position: absolute;
  top: var(--floor);
  left: 0;
  right: 0;
  bottom: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(90deg, rgba(90,40,20,.18) 0 1px, rgba(0,0,0,0) 1px 12px),
    repeating-linear-gradient(180deg, rgba(90,40,20,.18) 0 1px, rgba(0,0,0,0) 1px 8px);
}

/* White metro tiles behind the worktops. */
.mk-tiles{
  background:
    repeating-linear-gradient(180deg, rgba(0,0,0,0) 0 3px, #cdc8ba 3px 4px),
    repeating-linear-gradient(90deg, rgba(0,0,0,0) 0 7px, #cdc8ba 7px 8px),
    #f4f2ea;
}
.mk-sill{
  background: #f3efe4;
  box-shadow: 0 1px 0 #c9c1aa;
}
/* A striped runner in front of the sink and the hob. */
.mk-mat{
  background: repeating-linear-gradient(90deg, #3f5277 0 4px, #5d77a8 4px 8px);
  box-shadow: inset 0 0 0 1px #2e3f63;
}
/* The pendant over the board: faint in the day, the main light by evening. */
.mk-glow{
  background: radial-gradient(ellipse 50% 50% at 50% 30%,
    rgba(255,224,160,.42), rgba(255,224,160,0) 72%);
  mix-blend-mode: screen;
  opacity: .3;
  transition: opacity 900ms linear;
}
.stage--m-kitchen.is-evening .mk-glow,
.stage--m-kitchen.is-night .mk-glow{ opacity: 1; }
.stage--m-kitchen.is-evening .dk-window{
  background: linear-gradient(180deg, #33406c, #b9846e);
}
.stage--m-kitchen.is-night .dk-window{ background: #1d2544; }

/* --- The record room ----------------------------------------------------- */
/* Dusty blue walls and dark boards: a room for the evening, warm only where
   a lamp is. The music scene and the late one share all of it. */

.stage--m-music{
  --floor: 68%;
  --mm-wall: #a3b3c4;
  --mm-band: #8d9db0;
  --mm-floor: #7c5538;
  --mm-floor-dark: #5c3d28;
  --wall-band: #6f7f94;
  background:
    linear-gradient(180deg,
      var(--mm-wall) 0%,
      var(--mm-wall) calc(var(--floor) - 8%),
      var(--mm-band) calc(var(--floor) - 8%),
      var(--mm-band) var(--floor),
      var(--mm-floor) var(--floor),
      var(--mm-floor-dark) 100%);
}
.stage--m-music::before{
  content: '';
  position: absolute;
  top: var(--floor);
  left: 0;
  right: 0;
  bottom: 0;
  pointer-events: none;
  background: repeating-linear-gradient(90deg,
    rgba(0,0,0,.14) 0 1px, rgba(0,0,0,0) 1px 14px);
}
.stage--m-music .dk-window{
  background: linear-gradient(180deg, #33406c, #b9846e);
}
.stage--m-music.is-night .dk-window,
.stage--m-late .dk-window{
  background:
    radial-gradient(circle at 72% 28%, #f4f0d8 0 3px, rgba(244,240,216,0) 3.5px),
    #1b2341;
}
.mm-rug{
  background: #b8704a;
  box-shadow: inset 0 0 0 1px #8f5134, inset 0 0 0 3px #b8704a, inset 0 0 0 4px #e0a274;
}

/* Lamp, string lights and screen: over the hour's tint (z 950) and added with
   `screen`, like the bar's lanterns. */
.mm-glow{ mix-blend-mode: screen; }
.mm-glow--warm  { background: radial-gradient(closest-side, rgba(255,210,130,.44), rgba(255,210,130,0)); }
.mm-glow--string{ background: radial-gradient(closest-side, rgba(255,200,150,.30), rgba(255,200,150,0)); }
.mm-glow--screen{ background: radial-gradient(closest-side, rgba(120,190,255,.36), rgba(120,190,255,0)); }

/* --- Reduced motion ------------------------------------------------------ */
/* Nothing here has keyframes of its own (see the header). The lights still
   switch with the hour under reduce; they just switch rather than fade. */
@media (prefers-reduced-motion: reduce){
  .pt-flood,
  .pt-pool,
  .gf-mist,
  .mk-glow{ transition: none; }
}
