/* The hero: a warehouse at night, a breaker panel, and a neon sign.
   ============================================================================

   This file used to draw all of that in CSS - SVG filters, `paint-order:
   stroke fill` to fake a tube, four stacked `feGaussianBlur` layers, gradients
   standing in for metal. It now places images.

   That is not a retreat. It is the same argument the product makes: do the
   expensive, careful work once and off the critical path, then hand over a
   result that is cheap to use.

   The images come out of Blender. `render/blender/scene_hero.py` builds the
   whole scene from scratch in Python, so the asset is a SCRIPT, not a binary
   somebody exported once and can never change again. Real glass with real
   refraction, real emission, real bounce light, real haze in the air.

   PERFORMANCE. Large blur radii are among the most expensive things a browser
   repaints, and the old sign used several by construction. Every glow layer is
   now baked ONCE into a bitmap and only its `opacity` is animated - opacity
   composites on the GPU and never repaints. Nothing here animates a blur.

   And the phone never downloads the animation at all: it gets one square crop
   centred on the lit sign, and site.js does not insert the other plates below
   760px. One image instead of three, no power-up, no work.

   Regenerate everything: see docs/guides/render.md */

.shop {
  position: relative;
  display: block;
  max-width: none;
  padding: 0;
  background: var(--bg);
  overflow: clip;
}

/* --------------------------------------------------------------- the stage */

/* AS TALL AS THE WINDOW. The hero used to be a fixed 2.4:1 band, which meant
   that on any ordinary laptop it stopped two thirds of the way down and the
   next section started underneath it - the gap Sam circled.

   The render is 3:2 now and the stage is the height of the viewport less the
   navigation bar. 1.5 is not a taste decision: a maximised 16:10 laptop window
   is about 1.54 wide-to-tall once the nav is off it, and a picture flatter
   than the window it has to fill can only fill it by losing the sides.

   `100dvh`, not `100vh`: on a phone `vh` is the height WITHOUT the browser
   chrome, so a full-height hero sits under the address bar until you scroll. */
.shop-stage {
  position: relative;
  width: 100%;
  height: calc(100dvh - var(--nav-h, 75px));
  min-height: 380px;
  max-height: 1100px;
  background: #0d1013;
  overflow: hidden;
  isolation: isolate;        /* keep `screen` off everything behind the hero */
}

/* THE PICTURE'S OWN BOX, sized to cover the stage at the render's exact
   aspect. Everything positioned in PERCENT of the render lives in here - the
   plates and the lever overlay - so those percentages keep meaning what
   bake.py measured them to mean.

   This is the alternative to putting `object-fit: cover` on the plates, which
   would scale and crop each image independently of the overlay and leave the
   levers somewhere down the wall. Same reason the ultrawide crop used to be
   baked rather than done in CSS. */
/* Anchored to the TOP, not centred. A centred cover box trims the picture
   equally at both ends, which on a short window takes a slice off the
   headspace above the sign - the exact thing the render was reframed to
   protect. Anchoring the top means the only thing a short window ever loses
   is floor, and floor is what there is now a surplus of. */
.plate-box {
  position: absolute;
  top: 0;
  left: 50%;
  translate: -50% 0;
  aspect-ratio: 3 / 2;
  min-width: 100%;
  min-height: 100%;
}

.plate-box .plate {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: fill;
  will-change: opacity;
}

.plate-lit { opacity: 1; }
.plate-glow { opacity: 1; mix-blend-mode: screen; }

/* The two plates that exist only for the power-up. site.js inserts them, so
   they are absent from the document until something can actually use them. */
.plate-dark,
.plate-bays { opacity: 0; }

/* A little fall-off at the edges, so the copy panel has something to sit on
   and the full-bleed image does not end in a hard line against the page. */
.stage-vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to right, rgba(13,16,19,.55) 0%, rgba(13,16,19,0) 24%),
    linear-gradient(to top, rgba(13,16,19,.70) 0%, rgba(13,16,19,0) 28%);
}

/* ------------------------------------------------------------ the power-up */

.shop.is-off .plate-dark { opacity: 1; }
.shop.is-off .plate-glow { opacity: 0; }

.shop.is-bays .plate-dark { opacity: 0; transition: opacity .62s ease-out; }
.shop.is-bays .plate-bays { opacity: 1; transition: opacity .62s ease-out; }
.shop.is-bays .plate-glow { opacity: 0; }

.shop.is-lit .plate-dark { opacity: 0; }
.shop.is-lit .plate-bays { opacity: 0; transition: opacity .34s ease-out; }

/* A tube does not fade up. The gas has to ionise before it will conduct, so it
   stutters at the electrodes two or three times and then holds. Only opacity
   moves - the blur is already in the pixels. */
.shop.is-lit .plate-glow { animation: strike 900ms steps(1, end) forwards; }

@keyframes strike {
  0%   { opacity: 0; }
  8%   { opacity: .85; }
  13%  { opacity: .05; }
  20%  { opacity: .95; }
  25%  { opacity: .10; }
  33%  { opacity: 1; }
  100% { opacity: 1; }
}

/* ---------------------------------------------- the panelboard sprite: GONE

   There was a 12-frame sprite here - a BORDER RENDER of the panelboard at each
   handle angle - laid over the full-scene plate during the power-up so the
   levers appeared to throw. It is deleted, and the deletion is the fix.

   It never worked. It had to line up with a photograph at every viewport size,
   match that photograph's lighting exactly, and be handed off between two
   layers on a timed fade, and any one of those going wrong left a rectangle of
   the DARK panelboard sitting on a wall that had already lit up. It was patched
   for a stale crop, for two layers on different clocks, for a selector one
   class too specific, and for a transition that stalled and never finished.
   Every one of those was real and none of them was the problem.

   Sam, at the start: "ensure the entire full-scene background frame transitions
   uniformly rather than overlaying a cropped sub-asset image box over the
   canvas." That was the answer. The three plates already carry the panelboard -
   handles down in the dark one, up in the lit ones - so the whole sequence is
   full-frame cross-fades now and there is nothing left to misalign.

   What is lost: 480ms of lever movement nobody has ever seen render correctly.
   What is gained: a class of bug that cannot happen again, one fewer asset,
   one fewer generated stylesheet, and no per-render coordinates to keep in
   step. render/blender/scene_hero.py still renders the border frames because
   it writes hero.json in the same pass; that is dead weight and worth
   untangling, but it costs about a minute of a 25-minute render, not
   correctness. */

/* ------------------------------------------------------------- the copy

   Text ON the scene, not a slab in front of it.

   The previous treatment was an opaque bordered rectangle with its own drop
   shadow - a UI panel pasted over a photograph, which flattened the depth the
   render was built for. This is a soft directional scrim instead: the wall is
   darkened toward the right edge, where the render already leaves quiet wall,
   and the type sits on that. No border, no box, no shadow.

   Readability is not left to chance - the scrim is measured, not decorative,
   and the gradient reaches full strength behind the text block. */
.hero-copy {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: min(46%, 620px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(28px, 3.4vw, 64px) clamp(24px, 4vw, 76px)
           clamp(28px, 3.4vw, 64px) clamp(40px, 6vw, 120px);
  z-index: 2;
  background: linear-gradient(to right,
    rgba(13, 16, 19, 0) 0%,
    rgba(13, 16, 19, .62) 34%,
    rgba(13, 16, 19, .82) 68%,
    rgba(13, 16, 19, .88) 100%);
}

/* The measure applies to TEXT, not to the button row - constraining that too
   made two side-by-side buttons wrap into a column. */
.hero-copy > :where(h1, p) { max-width: 32ch; }

.hero-copy h1 {
  margin: 0 0 18px;
  font-size: clamp(28px, 3vw, 50px);
  line-height: 1.06;
  letter-spacing: -.025em;
  font-weight: 700;
  color: #fff;
}

.hero-copy .lede {
  margin: 0;
  font-size: clamp(15px, 1.15vw, 18px);
  line-height: 1.6;
  color: #cfd8e2;
  max-width: 38ch;
}

.hero-copy .cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 26px;
  max-width: none;
}

.hero-copy .fineprint { margin: 16px 0 0; font-size: 12.5px; color: #93a0ad;
                        max-width: 40ch; }

/* -------------------------------------------------------------- the reset */

.powerbtn {
  position: absolute;
  right: 12px;
  bottom: 10px;
  z-index: 3;
  padding: 7px 13px;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .06em;
  color: var(--dim);
  background: rgba(13, 16, 19, .72);
  border: 1px solid var(--line);
  cursor: pointer;
}

.powerbtn:hover { color: var(--text); border-color: var(--neon); }

/* ------------------------------------------------- what it does + the film */

/* THE SEAM. The hero is a full-bleed photograph and this is a padded band, so
   what sat between them was 96px of nothing: the section's own top padding,
   plus a 158px section head, plus its margin, before anything the visitor came
   for. That is the "blank space after the hero" - it is not a stray margin
   somewhere, it is the section starting a long way down.
   Top padding is now about a third of the bottom. A section ENDS with air and
   BEGINS close to what it follows; equal padding on both sides of a seam is
   what makes a page read as unrelated slabs stacked up. */
.capabilities {
  position: relative;
  padding-block: clamp(34px, 3.4vw, 56px) clamp(64px, 7.5vw, 116px);
}

.wrap.wide { max-width: var(--wrap-wide); }

/* SOMETHING FOR THE GLASS TO REFRACT. Two soft, large, low-contrast pools -
   warm off the neon at the top left, a cool one opposite - over the page
   colour. It is deliberately not a flat tint: a flat field blurs to itself
   and the cards go back to looking like white boxes.
   Warm rather than blue, because a blue field under white cards is the thing
   Sam called a dashboard. */
.capabilities::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(74% 56% at 14% 8%, rgba(255, 138, 43, 0.16), transparent 68%),
    radial-gradient(64% 52% at 88% 34%, rgba(18, 58, 97, 0.12), transparent 66%),
    radial-gradient(52% 44% at 46% 96%, rgba(255, 138, 43, 0.09), transparent 70%);
}
.capabilities > .wrap { position: relative; z-index: 1; }

/* One measure for every section head on the page, so the eye finds the start
   of a section in the same place every time. */
.section-head { max-width: 46ch; margin-bottom: clamp(24px, 2.8vw, 42px); }
/* 24ch, not 18: the heading is three short sentences and an 18ch measure
   broke it across three lines with "it." alone on the last one. */
.section-head h2 { margin: 0 0 12px; max-width: 24ch; }
.section-head .lede { margin: 0; color: var(--ink-2); font-size: 18px; }

/* ONE REGION: eight features and the film, not two sections that happen to be
   adjacent. The demo column is SIZED rather than left to the video, because
   the film is 9:16 - at 352px wide it is 626px tall, which is four rows of
   cards. Letting a portrait video set the height of a section is what left the
   dead space on both sides that Sam circled. */
.cap-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 352px;
  gap: clamp(22px, 2.6vw, 44px);
  align-items: start;
}

.cap-cells {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
}

/* EIGHT. Not three groups, not an accordion, not a paragraph.
   Separate boxes with their own padding and border - not cells in a joined
   table. Sam: "My earlier 4x2 instruction meant eight feature boxes, not a
   rigid joined table." They were merged into three "steps" once, which read
   as tidier and answered a question nobody had asked. */
/* GLASS - and the reason the last attempt looked like nothing.
   `backdrop-filter` blurs what is BEHIND the element. Behind these cards was a
   flat #f5f6f7. Blurring a flat colour returns the same flat colour, so the
   filter was running and doing precisely nothing; Sam could not tell the new
   cards from the old ones, and he was right. The fix is in two halves, and
   the second half is the one that matters.

   HALF ONE: the card itself, to Sam's own specification - 0.4 white, 15px
   blur, 20px radius, and the inner glow that actually reads as thick glass:
   `inset 0 0 14px 7px rgba(255,255,255,.7)`. The ::before and ::after are the
   lit top and left edges, which is how a glass panel catches a room.

   HALF TWO: `.capabilities::after`, further down - a soft wash behind the
   grid so the blur has something to find. Warm off the neon, not a blue tint
   under the whole page, because Sam's word for that was "dashboard". */
.cap {
  position: relative;
  margin: 0;
  padding: 18px 18px 20px;
  background: rgba(255, 255, 255, 0.40);
  -webkit-backdrop-filter: blur(15px) saturate(150%);
  backdrop-filter: blur(15px) saturate(150%);
  border-radius: 20px;
  border: 1px solid rgba(255, 255, 255, 0.30);
  box-shadow:
    0 8px 32px rgba(0, 0, 0, 0.10),
    inset 0 1px 0 rgba(255, 255, 255, 0.50),
    inset 0 -1px 0 rgba(255, 255, 255, 0.10),
    inset 0 0 14px 7px rgba(255, 255, 255, 0.70);
  overflow: hidden;
  transition: all 0.3s ease;
}

/* The lit top edge. */
.cap::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(90deg,
    transparent, rgba(255, 255, 255, 0.8), transparent);
  pointer-events: none;
}

/* And the lit left edge. */
.cap::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 100%;
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0.8), transparent, rgba(255, 255, 255, 0.3));
  pointer-events: none;
}

/* The scanner's corner brackets, as the page's recurring motif. Two corners,
   not four, so it reads as a detail rather than a frame. Static. */
.cap-brackets {
  position: absolute;
  inset: 10px;
  border-radius: 12px;
  pointer-events: none;
  background:
    linear-gradient(var(--line) 0 0) left 0 top 0 / 10px 1px no-repeat,
    linear-gradient(var(--line) 0 0) left 0 top 0 / 1px 10px no-repeat,
    linear-gradient(var(--line) 0 0) right 0 bottom 0 / 10px 1px no-repeat,
    linear-gradient(var(--line) 0 0) right 0 bottom 0 / 1px 10px no-repeat;
}

.cap-icon { display: block; width: 22px; height: 22px; color: var(--navy); }

.cap-id {
  margin: 12px 0 8px;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .13em;
  color: var(--dim);
}

.cap h3 {
  margin: 0 0 7px;
  font-size: 15.5px;
  font-weight: 650;
  line-height: 1.25;
  color: var(--ink);
  letter-spacing: -.01em;
}

/* Contrast, checked against the COMPOSITE the card now sits on rather than
   the flat page, because the card is 40% white over a warm wash. At the
   wash's strongest the effective surface is about #faefe7:
       --ink  on it  14.5:1        --dim  on it  5.2:1
   Both clear AA; the muted text moved from 5.64 to 5.2 and no further. If the
   wash is ever pushed harder, this is the number that goes first. */
.cap-note { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--dim); }

/* Sheen up, blur up, and two pixels of lift. The text does not move - only
   the card under it does. */
.cap:hover {
  background: rgba(255, 255, 255, 0.52);
  -webkit-backdrop-filter: blur(20px) saturate(170%);
  backdrop-filter: blur(20px) saturate(170%);
  border-color: rgba(255, 255, 255, 0.55);
  box-shadow:
    0 16px 40px rgba(0, 0, 0, 0.13),
    inset 0 1px 0 rgba(255, 255, 255, 0.70),
    inset 0 -1px 0 rgba(255, 255, 255, 0.16),
    inset 0 0 18px 9px rgba(255, 255, 255, 0.80);
  transform: translateY(-2px);
}
.cap:hover .cap-brackets,
.cap:focus-within .cap-brackets {
  background:
    linear-gradient(var(--navy) 0 0) left 0 top 0 / 14px 1px no-repeat,
    linear-gradient(var(--navy) 0 0) left 0 top 0 / 1px 14px no-repeat,
    linear-gradient(var(--navy) 0 0) right 0 bottom 0 / 14px 1px no-repeat,
    linear-gradient(var(--navy) 0 0) right 0 bottom 0 / 1px 14px no-repeat;
}

/* --------------------------------------------------------------- the film */

.demo { display: flex; flex-direction: column; gap: 12px; }

/* The slot is now just a slot. The black rectangle it used to be is gone -
   the device below is what the film sits in. */
.filmslot {
  position: relative;
  margin: 0;
  background: none;
  border: 0;
}

/* THE HANDSET. Bezel, screen, island, two keys, and a shadow it stands on.
   The screen clips the film to the inner curve, which is the detail that
   makes a rounded rectangle read as a phone rather than as a rounded box. */
/* NO FIXED ASPECT. The frame was 9:19.5 - a real handset - with a 9:16 film
   inside it, so `object-fit: cover` filled the extra height by zooming in and
   cut the top and bottom off the footage. The phone is SHORTER than a real
   one now and its screen is exactly the film's shape, so nothing is cropped:
   the device sizes itself to the video rather than the video being made to
   fit the device. Sam: it is fine if the phone is shorter, as long as the
   whole video is there. */
.phone {
  position: relative;
  padding: 11px;
  border-radius: 48px;
  background:
    linear-gradient(150deg, #4a5058 0%, #23272c 28%, #15181b 54%,
                    #2e3339 78%, #4a5058 100%);
  box-shadow:
    0 25px 50px -12px rgba(0, 0, 0, 0.25),
    inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}

.phone-screen {
  position: relative;
  aspect-ratio: 9 / 16;
  border-radius: 38px;
  overflow: hidden;
  background: #0d1013;
  isolation: isolate;
}

/* The poster is what a phone sees - the element has no playable source there -
   so it has to be framed exactly as the film is, or the still and the film
   would be two different crops of the same shot. */
.phone-screen .reel {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.phone-island {
  position: absolute;
  top: 19px;
  left: 50%;
  transform: translateX(-50%);
  width: 29%;
  height: 22px;
  border-radius: 14px;
  background: #07090b;
  pointer-events: none;
}

.phone-key {
  position: absolute;
  width: 3px;
  border-radius: 2px;
  background: linear-gradient(180deg, #4d545c, #2a2f35);
}
.phone-key-vol { left: -3px; top: 21%; height: 9%; }
.phone-key-pwr { right: -3px; top: 27%; height: 13%; }

.demo-note {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--dim);
}

/* ------------------------------------------------------ the closing section */

/* One connected composition. The version before this had a headline, a
   separated block of prose, three floating product thumbnails and a bordered
   card with its own drop shadow - four things with gaps between them, which
   read as an oversized block added to finish the page rather than as an
   invitation. */
.closing {
  position: relative;
  padding-block: clamp(62px, 7vw, 116px);
  overflow: hidden;
  isolation: isolate;
}

/* THE CAPTURE STATION IS THE BACKGROUND.
   It was a picture above the copy with a caption and a gutter either side.
   Sam: make the render the background and put the sign-up in front of it, and
   take the margins out. So the section is full bleed, the render covers it,
   and a gradient carries the copy - the same scrim the hero uses, for the
   same reason, because white text on a lit white sweep is not readable. */
.station-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: #0d1013;
}
.station-bg img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 58%;
}
.station-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to right,
    rgba(11, 14, 17, .90) 0%,
    rgba(11, 14, 17, .80) 42%,
    rgba(11, 14, 17, .62) 78%,
    rgba(11, 14, 17, .58) 100%);
}

/* The grid, and it sits ON the render. Merging `wrap` into this element took
   the gutters out of the middle of the section - Sam: eliminate the excess
   horizontal margin - while the background stays full bleed behind it. */
.closing-inner {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 380px);
  gap: clamp(32px, 5vw, 80px);
  align-items: center;
}

/* Light on dark, stated rather than inherited.

   Contrast worked out rather than eyeballed, because tools/contrast.py checks
   TOKEN pairs and these are literals over a photograph. Worst case is the lit
   white sweep behind the scrim: 10% of sRGB 230 over rgba(11,14,17) lands at
   about #232323.
       #ffffff  15.3:1      #cfd8e2  10.6:1      #a9b6c3  7.4:1
   All three clear AAA, and the scrim is heaviest on the left, which is where
   the copy is. `.dark-zone` redefines --text
   and --dim but not --ink-2, and the form's labels and fine print read from
   --ink-2 - which would have left them navy on near-black. */
.closing-copy h2 {
  max-width: 15ch;
  margin: 0 0 16px;
  font-size: clamp(1.9rem, 3.4vw, 2.7rem);
  color: #ffffff;
}
.closing-copy p {
  margin: 0;
  max-width: 46ch;
  font-size: 17px;
  line-height: 1.6;
  color: #cfd8e2;
}
.closing .field label { color: #cfd8e2; }
/* The fine print under the form, and the one place on this page where two
   rules fought and the wrong one won. There was a SECOND `.closing
   .fineprint` further down this file still setting `color: var(--ink-2)` -
   dark navy - and at equal specificity the later rule takes it. Over a
   near-black scrim that is unreadable, which is exactly how it shipped.
   The two rules are one rule now.

   #d1d5db on the scrim measures about 10.5:1, and it is carrying the only
   honest statement of what pressing the button does, so it had better be
   legible. */
.closing .fineprint {
  margin-top: 14px;
  font-size: 13px;
  line-height: 1.55;
  color: #d1d5db;
}
.closing .fineprint a { color: #ffffff; }

/* The form sits directly on the band. No card and no shadow: it is the only
   action in the section, so it does not need a container to be found. */
.waitlist { padding-top: 4px; }

.field { display: flex; flex-direction: column; gap: 7px; margin-bottom: 14px; }
.field label { font-size: 13.5px; font-weight: 600; color: var(--ink-2); }

.waitlist input::placeholder { color: var(--dim); }
.waitlist input:hover { border-color: var(--ink-2); }
.waitlist input[aria-invalid="true"] {
  border-color: var(--bad);
  background: var(--bad-wash);
}
.waitlist .btn { width: 100%; }

.formnote { margin: 14px 0 0; font-size: 13.5px; line-height: 1.5; }
.formnote.ok  { color: var(--ok-ink); }
.formnote.err { color: var(--bad-ink); }


/* Holds the two plates site.js inserts for the power-up. It has no box of its
   own - the plates inside it are absolute against the stage. */
.plate-slot { display: contents; }

/* =========================================================== responsive ==

   Four deliberate framings, not one layout squeezed. Every one of them keeps
   the hero plates and the panelboard overlay in the SAME coordinate system,
   because the overlay is positioned in percentages of the stage: crop one
   without the other and the levers end up somewhere down the wall.

   ---------------------------------------------------------------- laptop
   The default above. 2.4:1, copy scrim over the right. */

/* ------------------------------------------------------------ small laptop
   Below ~1080 the scrim would eat the sign, so the copy drops beneath the
   picture and gets the full measure. */
@media (min-width: 761px) and (max-width: 1080px) {
  .shop-stage { aspect-ratio: 2 / 1; }
  .hero-copy {
    position: static;
    width: auto;
    padding: clamp(24px, 4vw, 40px) var(--gutter);
    background: var(--bg);
  }
  .hero-copy > * { max-width: 46ch; }
  .hero-copy h1 { color: var(--ink); }
  .hero-copy .lede { color: var(--ink-2); }
  .hero-copy .fineprint { color: var(--dim); }
  /* The film moves under the features rather than shrinking beside them.
     A 9:16 video in a 240px column is a thumbnail, not a demonstration. */
  .cap-grid { grid-template-columns: 1fr; }
  .demo { max-width: 420px; width: 100%; margin-inline: auto; }
}

/* ---------------------------------------------------------------- phone

   A dedicated composition, not a shrunken desktop one.

   The hero is a square crop centred on the lit sign, the copy follows it on
   the page, and the film is replaced by a still. Nothing animates, nothing
   transitions, and no desktop asset is fetched: the plates are <picture> with
   `media`, and site.js returns before it touches the video. */
@media (max-width: 760px) {
  /* The phone plate is a SQUARE crop, not the 3:2 render, so the picture box
     is square here or the crop would be letterboxed inside a 3:2 frame. And
     the stage is sized to the picture rather than to the window - a
     full-height hero on a phone is a whole screen of wall before any words. */
  .shop-stage { height: auto; max-height: none; aspect-ratio: 1 / 1; }
  .plate-box { aspect-ratio: 1 / 1; }
  .plate-box .plate { object-fit: cover; }
  .plate-glow,
  .powerbtn { display: none; }

  .stage-vignette {
    background: linear-gradient(to top,
      rgba(13,16,19,.94) 0%, rgba(13,16,19,.30) 44%, rgba(13,16,19,0) 68%);
  }

  /* No negative overlap. Pulling the copy up over the picture put the first
     line of the headline behind the lit wall, where it was invisible. The
     copy follows the image; the vignette along the bottom of the crop does
     the joining. */
  .hero-copy {
    position: static;
    width: auto;
    padding: clamp(22px, 5vw, 32px) var(--gutter) clamp(28px, 6vw, 44px);
    margin-top: 0;
    background: none;
  }
  .hero-copy > * { max-width: none; }
  .hero-copy h1 { font-size: clamp(27px, 8vw, 36px); }
  .hero-copy .cta-row { flex-direction: column; align-items: stretch; }
  .hero-copy .btn { width: 100%; }

  /* The film comes FIRST on a phone - as a still, because the <source>
     does not match below 761px and nothing is fetched - and the eight
     features follow it in one column. */
  .cap-grid { grid-template-columns: 1fr; gap: 26px; }
  .demo { order: -1; max-width: 360px; width: 100%; margin-inline: auto; }
  .cap-cells { grid-template-columns: 1fr; gap: 12px; }

  .closing-inner { grid-template-columns: 1fr; }
  /* The phone gets the 4:3 crop of the station, and the section is short
     enough that the render fills it without a fixed ratio. */
  .station-bg img { object-position: center 55%; }

  /* Zero animation. Not "reduced" - none. A state change happens now. */
  .shop *, .capabilities *, .closing *, .page-head * {
    animation: none !important;
    transition: none !important;
  }
}

/* ------------------------------------------------------------- ultrawide
   There is no ultrawide rule any more, and that is the fix rather than an
   omission.

   It used to swap in a separately BAKED 3:1 crop past 1900px, because the
   stage was a fixed 2.4:1 band and a fixed band is taller than a wide window.
   The stage is now the height of the window with the picture covering it, so
   a 21:9 monitor crops the 3:2 render vertically all by itself - and it does
   it to the whole .plate-box at once, which is what keeps the lever overlay
   on the panelboard. The old rule also set `aspect-ratio: 3/1` on the stage,
   which would now fight the height it is supposed to fill.

   assets/hero/hero-wide-*.webp are still baked and no longer referenced. */

/* ------------------------------------------------------- reduced motion
   The markup already ships lit, so this only has to stop things moving. */
@media (prefers-reduced-motion: reduce) {
  .shop .plate { animation: none !important; transition: none !important; }
}

/* --------------------------------------------- the secondary detail page */

.page-head {
  padding-block: clamp(44px, 5vw, 76px) clamp(24px, 3vw, 40px);
  background: var(--band);
  border-bottom: 1px solid var(--line);
}
.page-head h1 {
  font-size: clamp(2rem, 4.6vw, 3.1rem);
  margin: 6px 0 16px;
  color: var(--ink);
}
.page-head .cap-id { margin: 0; color: var(--navy); }
.page-head .lede { max-width: 58ch; color: var(--ink-2); }

.closing-slim { text-align: left; }
.closing-slim h2 { max-width: 18ch; }
.closing-slim .lede { max-width: 52ch; margin-bottom: 4px; }

/* ------------------------------------------------------- shared proportions

   One set of rules the whole page obeys, so sections feel related through
   proportion rather than through a decorative treatment repeated on every
   element. Sam: "not through decorative brackets on every element."

   Measures:   section head 46ch, body 62ch, lede 44ch
   Rhythm:     section padding clamp(64px, 7.5vw, 120px)
   Corners:    0 on structure, 8px on controls. Nothing in between.
   Elevation:  hairlines. One shadow, on the primary button.            */

.wrap { max-width: var(--wrap); }

h2 { letter-spacing: -.025em; }

/* The seam between the dark warehouse and the light page. A hard edge between
   #14181c and the page is a line the eye reads as a mistake.

   This was `opacity: .07` over a full-strength gradient, which is 7% of a
   colour that is already close to the page: about one value step, which is
   to say nothing at all. The alpha lives in the gradient now and the value
   is one somebody chose. */
.after-dark { position: relative; }
.after-dark::before {
  content: "";
  position: absolute; left: 0; right: 0; top: 0;
  height: 96px;
  background: linear-gradient(to bottom,
    rgba(20, 24, 28, .11) 0%, rgba(20, 24, 28, .04) 46%,
    rgba(20, 24, 28, 0) 100%);
  pointer-events: none;
}
