/* engine.css - The Cafe Scene. Drop-in with engine.js; no build step, no libraries. */

.cs-root { position: relative; width: 100%; }

.cs-scroll {
  position: relative;
  width: 100%;
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  -ms-overflow-style: none;
  overscroll-behavior-x: contain;
}
.cs-scroll::-webkit-scrollbar { display: none; width: 0; height: 0; }

.cs-stage {
  position: relative;
  display: block;
  width: max(100%, 900px);
  /* aspect-ratio is written inline from the data's logical size */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  background: #000;
}

.cs-base {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  -webkit-user-drag: none;
}

/* ---- the foreground: the lips and edges things sit BEHIND ----
 * 🔴 ONE PICTURE OVER THE WHOLE STAGE, ABOVE EVERYTHING EXCEPT THE THING IN
 * YOUR HAND. It is the same room, the same size, see-through everywhere except
 * the front rails of the cubby floors, the shelf boards and the pour-over stand
 * - so a mug at rest, a mug sliding aside to make room and a mug flying home
 * are all tucked behind the rail, and only the one you are carrying comes out
 * in front of it. `z-index` is written inline by engine.js so the one number
 * lives with the layer numbers it has to sit between. */
.cs-fore {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  -webkit-user-drag: none;
}

/* ---- behind the window ----
 * The room picture has a hole where the glass is. This layer sits UNDER the
 * room at the window rect; the room's own painted frame is in front of it. Its
 * pictures are drawn 125% of the window's width and drift slowly; the video
 * fills the rect. `max-width: none` matters: the page that hosts the artifact
 * caps every picture at its box's width, which is exactly what squeezed the
 * first version of this (2026-09-08). */
.cs-behind {
  position: absolute;
  overflow: hidden;
  pointer-events: none;
  background: #0b1a2e;
}
.cs-behind img,
.cs-behind video {
  max-width: none;
  max-height: none;
  -webkit-user-drag: none;
}
.cs-behind-view {
  position: absolute;
  top: 0;
  left: -12.5%;
  width: 125%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  transition: opacity 2.4s ease;
  animation: cs-drift 40s ease-in-out infinite alternate;
}
.cs-behind-view.is-shown { opacity: 1; }
/* ---- the TV (2026-09-15) ----
 * The big frame over the right-hand door. Same arrangement as the window: a
 * hole in the room, this layer under it. Its children are a 16:9 box stretched
 * onto the mat's four corners by engine.js (width, height and transform are
 * written inline), so they are pinned to the top left with the origin there.
 * The video fades up over the still once it is really playing. */
.cs-tv {
  position: absolute;
  overflow: hidden;
  pointer-events: none;
  background: #07090c;
}
.cs-tv-still,
.cs-tv-video {
  position: absolute;
  top: 0;
  left: 0;
  max-width: none;
  max-height: none;
  object-fit: fill;
  transform-origin: 0 0;
  display: block;
  -webkit-user-drag: none;
}
.cs-tv-video { opacity: 0; transition: opacity 0.6s ease; }
.cs-tv-video.is-on { opacity: 1; }

/* ---- the TV's own control strip (2026-09-15) ----
 * A SEPARATE layer ON TOP of the room - the video itself is behind the room
 * picture, where a click could never reach it. Its face carries the same
 * matrix3d the video does, so the strip lies on the screen in perspective, and
 * everything in it is written in `em` of a font size engine.js sets from the
 * TV's height, so it stays in proportion at any stage size. */
.cs-tvui { position: absolute; pointer-events: none; }
.cs-tvui-face {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  pointer-events: auto;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.cs-tvbar,
.cs-tvclock {
  position: absolute;
  opacity: 0;
  transition: opacity 0.18s ease;
}
.cs-tvui.is-open .cs-tvbar,
.cs-tvui.is-open .cs-tvclock { opacity: 1; }
.cs-tvbar {
  left: 3%;
  right: 3%;
  bottom: 4%;
  display: flex;
  align-items: center;
  gap: 0.34em;
  padding: 0.24em 0.4em;
  background: rgba(6, 8, 11, 0.74);
  border-radius: 0.3em;
}
.cs-tvclock {
  right: 4%;
  top: 5%;
  color: #fff;
  font: 600 0.85em/1 ui-sans-serif, system-ui, sans-serif;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
  font-variant-numeric: tabular-nums;
}
.cs-tvbtn {
  flex: 0 0 auto;
  width: 1.5em;
  height: 1.5em;
  padding: 0;
  border: 0;
  background: transparent;
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font: 700 0.92em/1 ui-sans-serif, system-ui, sans-serif;
}
.cs-tvbtn svg { width: 100%; height: 100%; fill: currentColor; display: block; }
.cs-tvbtn:hover { color: #E31837; }
.cs-tvbtn:focus-visible { outline: 2px solid #E31837; outline-offset: 1px; }
.cs-tvrate { width: auto; min-width: 1.9em; font-variant-numeric: tabular-nums; }
.cs-tvplay .cs-i-play { display: none; }
.cs-tvplay.is-paused .cs-i-play { display: block; }
.cs-tvplay.is-paused .cs-i-pause { display: none; }
.cs-tvsnd .cs-i-slash { display: none; }
.cs-tvsnd.is-muted .cs-i-slash { display: block; stroke: #E31837; }
.cs-tvsnd.is-muted .cs-i-wave { display: none; }
.cs-tvtrack {
  flex: 1 1 auto;
  height: 1.5em;                /* a thumb-sized grab area round a thin line */
  display: flex;
  align-items: center;
  cursor: pointer;
  touch-action: none;           /* or a drag along the bar scrolls the page */
  min-width: 2em;
}
.cs-tvtrack::before {
  content: '';
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  margin-top: -0.14em;     /* or the grey line sits above the red one and the knob */
  height: 0.28em;
  border-radius: 0.14em;
  background: rgba(255, 255, 255, 0.3);
}
.cs-tvtrack { position: relative; }
.cs-tvfill {
  position: relative;
  height: 0.28em;
  border-radius: 0.14em;
  background: #E31837;
  pointer-events: none;
}
.cs-tvfill::after {
  content: '';
  position: absolute;
  right: -0.26em;
  top: 50%;
  width: 0.52em;
  height: 0.52em;
  margin-top: -0.26em;
  border-radius: 50%;
  background: #fff;
}
.cs-tvtrack:focus-visible { outline: 2px solid #E31837; outline-offset: 1px; }

/* ---- THE DOCKED STRIP, FOR FINGERS (2026-09-16) -------------------------------
 * Everything above draws the controls INSIDE the television, in perspective, in
 * `em` of the set's own height. That is right with a mouse and unusable with a
 * thumb: on an iPhone the whole television is about 100px wide, which leaves a
 * ~50px timeline and ~12px buttons. Where there is no hover, engine.js moves the
 * SAME bar into this dock - it is never copied, so there is one set of controls
 * and one paint() - and sizes it in real pixels instead. 🔴 It hangs off <body> and
 * is FIXED to the bottom of the screen: parked at the foot of the scene it was drawn
 * at y=785 on a 664px-tall phone, below the fold, because the room is taller than the
 * window and the television sits at the top of it.
 * ⚠️ These rules must stay AFTER `.cs-tvui.is-open .cs-tvbar`: that selector and
 * `.cs-tvdock .cs-tvbar` have the same specificity, so document order is what
 * decides which opacity wins. */
.cs-tvdock {
  display: none;
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 2147483000;
  flex-direction: column;
  align-items: stretch;
  padding: 6px 12px;
  padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px));
  background: rgba(6, 8, 11, 0.94);
  border-top: 1px solid rgba(255, 255, 255, 0.16);
}
.cs-tvdock.is-open { display: flex; }
.cs-tvdock .cs-tvbar {
  position: static;
  opacity: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  min-width: 0;
  gap: 6px;
  padding: 0;
  background: none;
  border-radius: 0;
  font-size: 16px;
}
/* 🔴 THE TIMELINE GETS A LINE OF ITS OWN. Sharing one row with three 44px buttons left it
 * 81px long on a 390px phone - measured - which is shorter than the thumb dragging it.
 * On its own line it is the width of the screen. */
.cs-tvdock .cs-tvtrack { flex: 1 0 100%; order: -1; height: 38px; min-width: 0; }
.cs-tvdock .cs-tvclock {
  position: absolute;
  right: 12px;
  bottom: 0;
  height: 44px;
  margin-bottom: calc(6px + env(safe-area-inset-bottom, 0px));
  display: flex;
  align-items: center;
  opacity: 1;
  font-size: 12px;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
/* 44px is the smallest thing a thumb hits reliably; the old buttons were ~12px. */
.cs-tvdock .cs-tvbtn { width: 44px; height: 44px; font-size: 15px; }
.cs-tvdock .cs-tvbtn svg { width: 24px; height: 24px; }
.cs-tvdock .cs-tvrate { width: auto; min-width: 44px; }
.cs-tvdock .cs-tvtrack { height: 44px; min-width: 0; }
.cs-tvdock .cs-tvtrack::before { height: 5px; margin-top: -2.5px; border-radius: 3px; }
.cs-tvdock .cs-tvfill { height: 5px; border-radius: 3px; }
.cs-tvdock .cs-tvfill::after {
  right: -10px; width: 20px; height: 20px; margin-top: -10px;
}
/* docked, the in-set strip must never draw as well */
.cs-tvui--docked .cs-tvbar, .cs-tvui--docked .cs-tvclock { display: none; }
.cs-behind-video {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
@keyframes cs-drift {
  from { transform: translateX(-9%); }
  to   { transform: translateX(9%); }
}

/* ---- Cafe behind the counter ----
 * One picture per art style stacked in one box; the shown one is opaque. A click
 * on him (his own pixels, checked in engine.js) cross-fades to the next style.
 * `max-width: none` for the same reason as the window: the host page caps every
 * picture at its box's width. */
.cs-barista,
.cs-barista-front {
  position: absolute;
  -webkit-tap-highlight-color: transparent;
}
.cs-barista-front { pointer-events: none; }
.cs-barista.is-over { cursor: pointer; }
.cs-barista:focus-visible { outline: 2px solid #E31837; outline-offset: 2px; }
.cs-barista-view,
.cs-barista-hand {
  position: absolute;
  display: block;
  max-width: none;
  max-height: none;
  opacity: 0;
  pointer-events: none;
  -webkit-user-drag: none;
  transition: opacity .28s ease;
}
.cs-barista-view { inset: 0; width: 100%; height: 100%; }
.cs-barista-view.is-shown,
.cs-barista-hand.is-shown { opacity: 1; }
.cs-barista.is-over .cs-barista-view.is-shown { filter: brightness(1.06); }
.cs-barista-tag {
  position: absolute;
  transform: translate(-50%, -115%);
  padding: 4px 10px;
  background: rgba(11, 11, 11, .88);
  border: 1px solid #E31837;
  color: #fff;
  font: 600 13px/1.2 Barlow, "Segoe UI", system-ui, sans-serif;
  letter-spacing: .03em;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity .2s ease;
}
.cs-barista-tag.is-shown { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .cs-barista-view, .cs-barista-hand, .cs-barista-tag { transition: none; }
}

/* ---- the chalkboard: a see-through button over the painted board ---- */
.cs-chalk {
  position: absolute;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.cs-chalk:focus-visible { outline: 2px solid #E31837; outline-offset: 2px; }

/* ---- items ---- */
/* 🔴 NO PERMANENT `will-change` HERE, AND THAT IS THE POINT. Promoting all 68
   items (and all 68 faces) to compositor layers for the life of the page costs
   every one of them its edge quality - a promoted layer is rasterised into a
   texture and composited, which draws slightly differently from the same thing
   drawn straight into the page. The hint is written inline by engine.js on the
   one item in flight and its partner, and taken off again on the landing frame,
   BEFORE the swap is recorded rather than after it. */
.cs-item {
  position: absolute;
  cursor: grab;
  touch-action: pan-y;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}
.cs-item:focus-visible { outline: 2px solid #E31837; outline-offset: 2px; }

.cs-face {
  position: absolute;
  inset: 0;
  transform-origin: 50% 60%;
}

/* 🔴 THE HINGE IS THE BOTTOM EDGE, AND IT LIVES HERE RATHER THAN INLINE. A
 * packet being laid down turns about the edge it is standing on. Writing the
 * origin in the stylesheet costs nothing when there is no transform — an origin
 * on its own changes no pixel — and it means the one frame the tip goes on is a
 * frame that writes ONE property instead of two.
 *
 * 🔴 THE SHELF-LEAN HANGS FROM THE SAME HINGE, AND THAT IS WHY THE FEET DO NOT
 * MOVE. A slot on a board that falls to the right is drawn with a `skewY` about
 * this same 50% 100% — the bottom CENTRE — so the point the object stands on is
 * the one point the lean leaves exactly where it was: the verticals stay
 * vertical, the bottom edge lies along the board, and nothing rises off the
 * shelf or sinks into it. engine.js writes the tip and the lean into one
 * transform string in one fixed order; see `poseTransform`. */
.cs-pose {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  transform-origin: 50% 100%;
}

/* 🔴 A SHAPE HANGS FROM A DIFFERENT HINGE FROM A LEAN, AND THAT IS WHY IT IS A
 * CLASS. The seven picture frames on the wall are not rectangles: the wall
 * recedes, so each cream mat is drawn as a trapezoid with its far edge shorter
 * than its near one. A picture resting in one is drawn through a `matrix3d`
 * worked out from the mat's four corners (engine.js `quadMatrix`), and a
 * homography is written from the origin outward — so the origin has to be the
 * TOP LEFT, where a lean's has to be the bottom centre so a mug's feet stay on
 * its shelf.
 *
 * ⚠️ NOTHING WEARS BOTH. A picture hangs on a wall and rests on nothing, so no
 * picture slot carries a lean; everything that leans stands on a board and is a
 * rectangle. engine.js writes one or the other and adds this class with it. */
.cs-item.cs-warp .cs-pose { transform-origin: 0 0; }

.cs-item img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
  -webkit-user-drag: none;
  pointer-events: none;
}
.cs-item[data-family="pic"] img { object-fit: cover; }

/* ---- standing up and lying down ----
 * 🔴 TWO DRAWINGS, ONE SHOWING. An object that has been drawn lying down as
 * well as standing up carries both pictures, and the class says which one is
 * out. The other is at zero opacity rather than `display: none`, so the browser
 * has decoded it long before the swap — the picture changes in the MIDDLE of a
 * flight, where a packet arriving a frame late is the most visible fault on the
 * page.
 *
 * ⚠️ SCOPED TO `.cs-two`, WHICH ONLY THE TWO-PICTURE OBJECTS CARRY. The other
 * 60 items get no second element, no stacking and no opacity — the page is
 * identical for them, which is the whole point of the scoping. */
.cs-pose.cs-two img { position: absolute; left: 0; top: 0; }
.cs-pose.cs-two .cs-flat { opacity: 0; }
.cs-item.cs-lay .cs-pose.cs-two .cs-up { opacity: 0; }
.cs-item.cs-lay .cs-pose.cs-two .cs-flat { opacity: 1; }

/* ⚠️ SCOPED SO A GESTURE CAN PARK IT. engine.js adds `.cs-nohover` on
 * pointer-down and takes it off again on the next real mouse move, so the
 * mouse-over glow cannot appear from underneath the carried styling in the one
 * frame the new arrangement is recorded. */
.cs-root:not(.cs-nohover) .cs-item:hover .cs-face { filter: brightness(1.08); }
.cs-item.cs-carry { cursor: grabbing; z-index: 999 !important; }

/* ---- the lift and the ring ----
 * 🔴 BOTH ARE NUMBERS THAT RAMP, NOT CLASSES THAT SWITCH, AND THE CLASS COMES
 * OFF THE MOMENT BOTH REACH ZERO. The landing has to produce a frame that is
 * pixel-for-pixel identical to the one before it, and a highlight still fading
 * when the scene rearranges puts a change of colour on exactly that frame. So
 * engine.js steps `--cs-lift` and `--cs-tgt` on the same clock as everything
 * else, refuses to begin the landing until both are 0, and then removes
 * `.cs-lit` so no filter is left standing on the element at all.
 *
 * ⚠️ AND THE FILTER IS SCOPED TO `.cs-lit` RATHER THAN LIVING ON EVERY FACE. A
 * filter - even one whose every term is a no-op - gives the element its own
 * paint context for the life of the page. 68 of those is the same mistake as a
 * permanent `will-change`. */
.cs-item.cs-lit .cs-face {
  filter:
    brightness(calc(1 + .35 * var(--cs-lift, 0) + .12 * var(--cs-tgt, 0)))
    drop-shadow(0 calc(10px * var(--cs-lift, 0)) calc(14px * var(--cs-lift, 0))
                rgba(0, 0, 0, calc(.55 * var(--cs-lift, 0))))
    drop-shadow(0 0 calc(3px * var(--cs-tgt, 0)) rgba(227, 24, 55, calc(.9 * var(--cs-tgt, 0))))
    drop-shadow(0 0 calc(9px * var(--cs-tgt, 0)) rgba(227, 24, 55, calc(.85 * var(--cs-tgt, 0))));
}

/* ---- reset link ---- */
.cs-reset {
  display: inline-block;
  margin-top: 10px;
  font-size: 13px;
  color: #cfcfcf;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  font-family: inherit;
}
.cs-reset:hover { color: #E31837; }

/* ---- edit mode ---- */
.cs-editbox {
  position: absolute;
  z-index: 1000;
  border: 2px solid currentColor;
  background: rgba(0,0,0,.12);
  cursor: move;
  touch-action: none;
  box-sizing: border-box;
}
.cs-editbox .cs-editlabel {
  position: absolute;
  left: 0;
  top: -14px;
  font: 11px/1.1 ui-monospace, "IBM Plex Mono", Menlo, Consolas, monospace;
  color: currentColor;
  background: rgba(0,0,0,.75);
  padding: 1px 3px;
  white-space: nowrap;
  pointer-events: none;
}
.cs-editbox .cs-edithandle {
  position: absolute;
  right: -6px;
  bottom: -6px;
  width: 12px;
  height: 12px;
  background: currentColor;
  cursor: nwse-resize;
}
.cs-editpanel { margin-top: 14px; }
.cs-editpanel textarea {
  width: 100%;
  box-sizing: border-box;
  min-height: 220px;
  background: #0B0B0B;
  color: #eaeaea;
  border: 1px solid #222;
  padding: 10px;
  font: 13px/1.45 ui-monospace, "IBM Plex Mono", Menlo, Consolas, monospace;
}
.cs-editpanel button {
  margin-top: 8px;
  background: #E31837;
  color: #fff;
  border: 0;
  padding: 8px 14px;
  font: 13px/1 inherit;
  cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
  .cs-behind-view { animation: none; transition: none; }
}
