/* =============================================================================
   360sona — the front door
   Target: Xbox.com, 2009–2010. Provenance marks as in tokens.css.

   [M] the ground and the greens are the same measured values the editor uses.
       docs/period-sources.md: the studio backdrop is a perfectly neutral grey
       (max channel spread 2 over the whole 1921x1081 image), so Microsoft put NO
       colour in the ground and neither does this. The Buy Now button is #7FAD2A
       fill / #336600 ink / #ABEF2E hover TEXT out of 2009 Xbox.com CSS, and all
       three fill greens fail WCAG for text, which is why green here is a fill
       and never a paragraph.

   [D] the gloss, the radii and every duration. Brief §7.2: the 2009 homepage
       hero was Flash and nothing about its interior survives. The vocabulary is
       period; the stops are decisions.

   NO BOXES. An earlier pass put every figure in a bordered plate with a drop
   shadow, which made the avatars read as stock photographs pasted onto a page.
   They render on the page's own ground, transparent, with nothing around them.
   That is also what the app does: the editor's stage has no frame either.
   ============================================================================= */

/* NOT IN A LAYER, and that is deliberate for the same reason 360sona.css is not.
   Unlayered CSS outranks every layer, so a sheet inside `@layer components`
   loses to any unlayered rule of equal specificity -- and 360sona.css is
   unlayered by design. In `@layer components` this file could not turn off the
   era's h2 hairline, which is right in the editor's panes and reads as a stray
   divider across a setpiece. Loaded after it, unlayered, later simply wins. */

#front {
  position: fixed;
  inset: 0;
  z-index: 60;
  overflow-y: auto;
  overflow-x: hidden;
  background:
    linear-gradient(to bottom, #cccccc 0, #e2e2e2 18%, #f2f2f2 42%,
                    #eaeaea 64%, #dcdcdc 100%);
  color: var(--ink-body);
  font: var(--type-body);
  -webkit-font-smoothing: antialiased;
}

/* ---------------------------------------------------------------- the bar - */

/* THE BAR IS THE SAME BAR ON EVERY SCREEN.
   Three copies of it exist because the front door, the flat pages and the
   editor load three different stylesheets and always have -- a document should
   not pull in the editor's CSS to use four rules of it. What they must not do
   is drift, and they had: the flat pages ruled their bar in #D9D9D9 while the
   front door used the measured #BCBCBC, the gaps were 18px against var(--sp-9),
   and the editor's bar had no ground, no rule and no shadow at all, so it read
   as a different site. These five declarations are the bar; change them in all
   three places or in none. */
.fbar {
  position: sticky;
  top: 0;
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 10px 30px;
  background: linear-gradient(#ffffff, #f5f5f5 54%, #ececec);
  border-bottom: 1px solid #BCBCBC;
  box-shadow: 0 1px 3px rgb(0 0 0 / .12);
}
.fsp { flex: 1; }

/* The close on the sign-in page: a square, not a word. It sits where every
   dialog's close has sat since dialogs existed, and it goes back to whatever
   was underneath rather than to one fixed page. */
.afx {
  width: 34px; height: 34px;
  padding: 0;
  font: 400 22px/1 var(--ui);
  display: grid; place-items: center;
}

/* --- the mark --------------------------------------------------------------
   ONE PICTURE for the bust: a real render of the blank figure cut at its own
   measured neck, with Microsoft's sphere composited over the collar at build
   time. Two CSS-positioned layers drifted apart at every size nobody tuned by
   hand; a composite cannot. */
.flogo { display: flex; align-items: center; gap: 12px; }
.fbust { display: block; flex: 0 0 auto; line-height: 0; }
.fbust img {
  display: block;
  height: 46px;
  width: auto;
  filter: drop-shadow(0 1px 2px rgb(0 0 0 / .22));
}
.fwm { display: block; line-height: 0; }
.fwm svg { display: block; height: 26px; width: auto; }
.fwm.dim { opacity: .75; }
.fwm.dim svg { height: 18px; }

.fnav { display: flex; gap: var(--sp-9); }
.fnavlink {
  color: var(--green-ink);
  text-decoration: none;
  font: var(--type-meta);
  letter-spacing: 0;
}
.fnavlink:hover { text-decoration: underline; }

/* The three that belong at the end of a page, at the end of the page. */
.flinks { display: flex; flex-wrap: wrap; gap: 18px; margin-top: 10px; }
.flinks a { color: var(--green-ink); text-decoration: none; font: var(--type-secondary); }
.flinks a:hover { text-decoration: underline; }

.fnav a {
  color: var(--green-ink);
  text-decoration: none;
  font: var(--type-meta);
  padding: 4px 2px;
  border-bottom: 2px solid transparent;
}
.fnav a:hover { border-bottom-color: var(--green-brand); }

.fwho { display: flex; align-items: center; gap: var(--sp-7); }
.fname { font: var(--type-meta); color: var(--ink); }

/* ------------------------------------------------------------- the buttons - */

.fbtn {
  font: var(--type-meta);
  padding: 7px 14px;
  border-radius: var(--r-button);
  border: 1px solid var(--border-heavy);
  background: linear-gradient(#ffffff, #f0f0f0 50%, #e2e2e2);
  color: var(--ink);
  cursor: pointer;
  box-shadow: var(--shadow-rest);
}
.fbtn:hover { background: linear-gradient(#ffffff, #f6f6f6 50%, #ececec); }
.fbtn:active { box-shadow: var(--shadow-press); transform: translateY(1px); }
.fbtn.big { font-size: 15px; padding: 12px 26px; }

/* The Buy Now button, to its measured values. */
.fbtn.go {
  background: linear-gradient(#9ccb43 0, #7FAD2A 52%, #6d9722 100%);
  border-color: var(--green-ink);
  color: #ffffff;
  text-shadow: 0 1px 0 rgb(0 0 0 / .30);
}
.fbtn.go:hover { color: var(--green-hover); }
.fbtn.quiet {
  background: transparent; border-color: transparent;
  color: var(--green-ink); box-shadow: none;
}
.fbtn.quiet:hover { text-decoration: underline; }
.fbtn.grey { color: var(--ink-body); }

/* --------------------------------------------------------------- the hero - */

.fhero {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 470px;
  gap: 30px;
  align-items: center;
  max-width: 1180px;
  margin: 0 auto;
  padding: 46px 30px 26px;
}
.fhtext { min-width: 0; position: relative; z-index: 2; }

/* NOT SET IN CAPITALS. The era's own labels were checked -- twenty distinct NXE
   strings, not one of them all-caps -- and where it touched tracking at all it
   TIGHTENED display type rather than opening small text out. */
.feyebrow {
  font: var(--type-meta);
  letter-spacing: 0;
  color: var(--ink-caption);
  margin: 0 0 12px;
}
.fh1 {
  margin: 0 0 14px;
  font: 700 46px/1.04 var(--ui);
  color: var(--ink);
  letter-spacing: -.015em;
}
.fsub {
  margin: 0 0 22px;
  max-width: 34ch;
  font-size: 16px;
  line-height: 1.5;
  color: var(--ink-body);
}
.fdo { display: flex; gap: var(--sp-8); flex-wrap: wrap; }
.ffine {
  margin: 14px 0 0;
  max-width: 42ch;
  font: var(--type-secondary);
  color: var(--ink-caption);
}

/* --- the wall ---------------------------------------------------------------
   Real thumbnails, faint, behind everything. Twenty thousand items is a claim
   until some of them are on screen. Masked at the edges so it fades into the
   ground rather than stopping at a rectangle. */
.fwall {
  position: absolute;
  inset: -10px 0;
  z-index: 0;
  opacity: .13;
  pointer-events: none;
  overflow: hidden;
  -webkit-mask-image: radial-gradient(120% 100% at 50% 50%, #000 25%, transparent 78%);
  mask-image: radial-gradient(120% 100% at 50% 50%, #000 25%, transparent 78%);
}
.fwall canvas { width: 100%; height: 100%; display: block; object-fit: cover; }

/* --- the figure, on the page and not in a frame --------------------------- */
.fstage {
  position: relative;
  z-index: 1;
  aspect-ratio: 3 / 4;
  min-height: 440px;
  margin-bottom: 18px;
}
.fstage canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  /* The orbit's far half sits at 0 and its near half at 2, so the ring passes
     behind the figure and back out in front of it. */
  z-index: 1;
}
.fcap {
  position: absolute;
  left: 50%; bottom: -16px;
  transform: translateX(-50%);
  max-width: 92%;
  font: var(--type-meta);
  color: var(--ink-dim);
  text-shadow: 0 1px 0 rgb(255 255 255 / .9);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none;
}

/* --- items in orbit --------------------------------------------------------
   Real thumbnails going round the figure on a shallow tilted ellipse. The far
   half genuinely passes BEHIND the avatar: the canvas is at z-index 1 and each
   thumbnail takes 0 or 2 by which side of the orbit it is on, so this needs
   .fstage to be the stacking context and #fFly to create none of its own.
   Position and scale come from the loop in front.js; only the look is here. */
.ffly {
  position: absolute; inset: 0; pointer-events: none;
  /* The ring is placed in container units so the loop in front.js writes one
     transform and nothing else -- no percentage left/top, which are layout. */
  container-type: size;
}
/* Two halves at fixed depths either side of the canvas (z-index 1). An item
   moves between them a few times a minute; it used to have its z-index
   rewritten sixty times a second, which restacks. */
.fring { position: absolute; inset: 0; }
.fring.back { z-index: 0; }
.fring.front { z-index: 2; }
.fi {
  position: absolute;
  left: 50%; top: 50%;
  width: 78px; height: 78px;
  object-fit: contain;
  transform: translate(-50%, -50%);
  /* NO drop-shadow. A filter on an element whose transform changes every frame
     re-renders the filter every frame, off the compositor's fast path, five
     times over. The ring reads fine without one. */
  will-change: transform, opacity;
}
@media (prefers-reduced-motion: reduce) { .ffly { display: none; } }

/* --------------------------------------------------------------- the numbers */

.fnums {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  padding: 14px 30px 18px;
  border-bottom: 1px solid var(--rule-light);
}
.fnums div {
  display: flex; flex-direction: column; align-items: center;
  min-width: 132px; padding: 0 22px;
  border-left: 1px solid var(--rule-light);
}
.fnums div:first-child { border-left: 0; }
.fnums b {
  font: 700 26px/1.1 var(--ui);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.fnums span { font: var(--type-secondary); color: var(--ink-caption); margin-top: 2px; }

/* ------------------------------------------------------------ the setpieces */

.fshow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 440px;
  gap: 40px;
  align-items: center;
  max-width: 1180px;
  margin: 0 auto;
  padding: 10px 30px 0;
}
.fshow.flip { grid-template-columns: 440px minmax(0, 1fr); }
.fshow.flip .fw { order: 2; }
.fshow.flip .fart { order: 1; }
.fshow.wide { grid-template-columns: minmax(0, 1fr); }

.fw h2 {
  margin: 0 0 8px;
  font: 700 30px/1.12 var(--ui);
  color: var(--ink);
  /* No hairline. base.css gives every h2 the era's heading rule, which is right
     inside the editor's panes and reads as a stray divider across a setpiece. */
  border-bottom: 0;
  padding-bottom: 0;
}
.fw p { margin: 0; max-width: 40ch; font-size: 15px; line-height: 1.5; color: var(--ink-body); }
.fread {
  margin-top: 12px !important;
  font: var(--type-debug);
  color: var(--ink-caption);
  white-space: pre;
}

.fart { position: relative; aspect-ratio: 4 / 5; min-height: 360px; }
/* A PROP NEEDS ROOM SIDEWAYS. Excalibur arrives with a plinth, the greatshield
   is taller than the person behind it and the axe bass gets swung overhead --
   the camera is solved against each prop's own swept box, and a 4:5 column
   would make the solver pull back until everything was small. */
.fart.wide { aspect-ratio: 5 / 4; min-height: 420px; }
.fshow.props { grid-template-columns: 560px minmax(0, 1fr); }
/* Same shape as the props section and for the same reason: the subject is wide
   and turning, so it gets the room and the words get the column. */
.fshow.models { grid-template-columns: 480px minmax(0, 1fr); }

/* The file, as facts. Two columns, figures right-aligned on their own axis, so
   the numbers changing every fifteen seconds read as one thing being measured
   rather than as text being replaced. */
.fmstat {
  margin: 18px 0 0;
  display: grid;
  grid-template-columns: auto auto;
  justify-content: start;
  gap: 2px 18px;
  font: var(--type-debug);
}
.fmstat dt { color: var(--ink-caption); }
.fmstat dd {
  margin: 0;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}


/* WHAT IS ON THE STAGE RIGHT NOW, and it must not read as a second heading.
   The section title is bold, dark and 30px; a 26px bold name directly under it
   looked like the same thing said twice. This is the other half of every type
   contrast there is: light weight, wider tracking off, and set against a green
   rule that marks the block as a caption for the picture rather than a heading
   for the section. */
.fnow {
  margin-top: 20px;
  padding-left: 14px;
  border-left: 3px solid var(--green-brand);
}
.fpwhere {
  display: block;
  font: var(--type-meta);
  letter-spacing: 0;
  color: var(--green-ink);
}
.fpname {
  display: block;
  margin-top: 1px;
  font: 300 30px/1.2 var(--ui);
  color: var(--ink);
  letter-spacing: -.01em;
}
.fart canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* THE WIREFRAME ROW is one canvas holding five figures in sub-viewports, so it
   is sized as a band rather than a column. Full width, its own proportion, and
   no other row on this page looks like it. */
.fwires {
  grid-column: 1 / -1;
  position: relative;
  width: 100%;
  aspect-ratio: 1240 / 492;
  margin-top: 4px;
}
.fwires canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.fshow.wire { padding-bottom: 14px; }

/* --- the costume marquee ---------------------------------------------------
   A row that moves on its own rather than a scroller nobody scrolls. The track
   holds the run twice and travels exactly half its width, so the loop point
   lands with the second copy where the first one was and there is no seam.

   The mask is the other half: figures fade in at one end and out at the other
   instead of being clipped mid-costume by the edge of the window. */
.fmarquee {
  grid-column: 1 / -1;
  position: relative;
  overflow: hidden;
  padding: 10px 0 16px;
}
/* THE FADE IS TWO OVERLAYS, NOT A MASK.
   mask-image on a box whose child is transform-animated forces the compositor
   to render that child to an offscreen buffer and mask it every frame, and the
   child here is a five-thousand-pixel-wide track. Two static gradients painted
   over the top are the same picture and are rasterised once. */
.fmarquee::before, .fmarquee::after {
  content: '';
  position: absolute; top: 0; bottom: 0; width: 9%;
  pointer-events: none; z-index: 2;
}
/* AND THE COLOUR IS THE PAGE'S OWN GROUND, MEASURED, NOT A FLAT GREY.
   These were hard-coded #eeeeee against a body that is a five-stop vertical
   gradient running #cccccc to #dcdcdc. At the costume row's depth the ground is
   nowhere near #eeeeee, so the two fades read as the lit edges of a white panel
   sitting on the gradient -- the box this page is not supposed to have.
   --fade is written by front.js from the gradient's own stops at this element's
   position, so the fade is the ground and is therefore invisible. */
.fmarquee::before { left: 0; background: linear-gradient(90deg, var(--fade, #eeeeee), transparent); }
.fmarquee::after { right: 0; background: linear-gradient(270deg, var(--fade, #eeeeee), transparent); }
.ftrack { will-change: transform; }
.ftrack {
  display: flex;
  gap: 10px;
  width: max-content;
  animation: march calc(var(--n, 10) * 4.2s) linear infinite;
}
.fmarquee:hover .ftrack { animation-play-state: paused; }
@keyframes march { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .ftrack { animation: none; } }

/* --- the collection wall ----------------------------------------------------
   THREE ROWS SLIDING AGAINST EACH OTHER, which is the whole idea.

   One row of logos is a "trusted by" strip and reads as a list of nine things.
   Three rows moving in opposite directions read as a SURFACE, and the shear
   between them is what says there is more of this than fits on the screen --
   which is the claim the section is making. The middle row is offset half a
   tile so the columns never settle into a grid.

   Durations come from front.js, not from here: the rows hold different numbers
   of icons, so one shared duration would give three different speeds. Each row
   gets `--dur` computed from its own measured width against the costume
   marquee's own 42.4 px/s. */
.fband {
  grid-column: 1 / -1;
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px 0 18px;
  /* The rows are wider than the page by design; nothing here scrolls. */
  contain: paint;
}
.fbrow { overflow: hidden; }
.fbtrack {
  display: flex;
  gap: 8px;
  width: max-content;
  animation: bandmarch var(--dur, 120s) linear infinite;
  will-change: transform;
}
/* The second row runs the other way. Same keyframes, reversed, so the two can
   never drift out of step with each other the way two hand-written animations
   would. The half-tile offset is what stops the three rows lining up. */
.fbrow.back .fbtrack {
  animation-direction: reverse;
  margin-left: -28px;
}
@keyframes bandmarch { to { transform: translateX(-50%); } }
.fband:hover .fbtrack { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) { .fbtrack { animation: none; } }

.fbt {
  flex: 0 0 auto;
  width: 48px; height: 48px;
  border-radius: 7px;
  overflow: hidden;
  /* A hairline and an inset highlight: the same two edges every tile in the
     2009 dashboard had, and what stops seven hundred squares reading as one
     grey noise field. */
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / .16),
              inset 0 1px 0 rgb(255 255 255 / .35);
  background: rgb(255 255 255 / .35);
}
.fbt img {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  /* HELD BACK, THEN LET GO ON HOVER. At full colour seven hundred publisher
     logos is a ransom note -- every one of them was designed to be the loudest
     thing on its own box. Muted, they become a texture, which is what the
     section is about; putting the pointer on the band brings them all back. */
  filter: saturate(.55) contrast(.92);
  opacity: .82;
  transition: filter 420ms var(--ease-out), opacity 420ms var(--ease-out);
}
.fband:hover .fbt img { filter: none; opacity: 1; }
.fbt.gone { box-shadow: inset 0 0 0 1px rgb(0 0 0 / .08); background: rgb(0 0 0 / .03); }
.fbt.gone img { display: none; }

/* Both ends fade into the page's own ground, the same way the costume row
   does, so the wall runs off the edges instead of being cut off at them. */
.fband::before, .fband::after {
  content: '';
  position: absolute; top: 0; bottom: 0; width: 11%;
  pointer-events: none; z-index: 2;
}
.fband::before { left: 0; background: linear-gradient(90deg, var(--fade, #eeeeee), transparent); }
.fband::after { right: 0; background: linear-gradient(270deg, var(--fade, #eeeeee), transparent); }

/* The section is a band under a heading, not two columns. */
.fshow.band { padding-bottom: 6px; }

.frow, .ftrack figure { margin: 0; }
.ftrack figure, .frow figure {
  margin: 0; flex: 0 0 auto; width: 168px; text-align: center;
}
.frow {
  grid-column: 1 / -1;
  display: flex; gap: 10px; overflow-x: auto;
  padding: 10px 0 16px; scrollbar-width: thin;
}
.frow.small figure { width: 165px; }
.ftrack img, .frow img {
  display: block; width: 100%; height: 224px; object-fit: contain;
  opacity: 0; transition: opacity 320ms cubic-bezier(.1,.9,.2,1);
}
.frow.small img { height: 210px; }
.ftrack img.in, .frow img.in { opacity: 1; }
.ftrack figcaption, .frow figcaption {
  font: var(--type-secondary);
  color: var(--ink-caption);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* --- still rendering -------------------------------------------------------
   The loading screen comes down when the HERO is up, because holding it until
   the whole page is built is another ten seconds of nothing. The sections
   underneath are still filling for a few seconds after that, and an empty
   rectangle where a figure will be reads as broken. A quiet sweep says the
   section knows it is empty. */
/* --- waiting ---------------------------------------------------------------
   THE MARK, TURNING, OVER NOTHING.

   What was here was a white shimmer sweeping across an empty rectangle, which
   is the visual language of a broken image, not of work in progress -- and the
   hero had no state at all: it arrived, hitched while the other four sections
   fitted their cameras, and settled. Now every stage waits under the sphere.

   NO GROUND. The cover paints nothing of its own, so the page's gradient runs
   straight through it. A panel here would be exactly the box this page spends
   its whole stylesheet not having. */
.floading { position: relative; }
/* The canvas is held back rather than drawn over: a partially built figure is
   worse than no figure, and fading it in on arrival costs nothing. */
.floading > canvas, .floading > .ftrack { opacity: 0; }
/* Scoped to #front on purpose: front.css is unlayered, so a bare `canvas` rule
   here would also catch the editor's own stage underneath. */
#front canvas, #front .ftrack { transition: opacity 340ms cubic-bezier(.1,.9,.2,1); }

.fcover {
  position: absolute; inset: 0; z-index: 3;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 10px;
  pointer-events: none;
  transition: opacity 380ms cubic-bezier(.1,.9,.2,1);
}
.fcover.out { opacity: 0; }
/* A PLAIN RING, NOT THE MARK. A brand logo in motion reads as a splash screen,
   and five of them turning in five sections of one page reads as five splash
   screens. This is a loading spinner: nobody has to decode it. The green is the
   page's own fill green, and the track is the same hairline the rules use. */
.fspin {
  display: block;
  width: 34px; height: 34px;
  border-radius: 50%;
  border: 3px solid rgb(0 0 0 / .12);
  border-top-color: var(--green-brand, #7FAD2A);
  animation: fspin .72s linear infinite;
  will-change: transform;
}
@keyframes fspin { to { transform: rotate(360deg); } }
.fcapt {
  font: var(--type-meta);
  letter-spacing: 0;
  color: var(--ink-caption);
  font-weight: 400;
}
@media (prefers-reduced-motion: reduce) {
  .fspin { animation-duration: 2.4s; }
}

/* --------------------------------------------------------------- the foot - */

/* AND THE SAME FOOTER. Centred, 13px, one rule colour -- see the note on
   .pfoot, which is this rule's other copy. */
.ffoot {
  margin-top: 22px;
  padding: 22px 30px 40px;
  border-top: 1px solid #E2E2E2;
  display: flex; align-items: center; gap: 8px 18px;
  flex-wrap: wrap; justify-content: center; text-align: center;
  font-size: 13px; color: var(--ink-dim);
}
.ffoot p { margin: 0; max-width: 60ch; font: var(--type-secondary); color: var(--ink-caption); }

/* --------------------------------------------------------------- signing in */

/* THE WHOLE WINDOW. Two columns, the form on the left and a live avatar on the
   right, on the same ground and under the same bar as the front page. It used
   to be three hundred pixels of the editor's left column with the editor it was
   gatekeeping visible around it. */
.fauth {
  display: grid;
  grid-template-columns: minmax(0, 440px) minmax(0, 1fr);
  gap: 60px;
  align-items: center;
  max-width: 1080px;
  margin: 0 auto;
  padding: 40px 30px 60px;
  min-height: calc(100vh - 70px);
}
.flogo.asbtn {
  background: none; border: 0; padding: 2px 6px 2px 0; cursor: pointer;
  border-radius: var(--r-button); font: inherit; color: inherit;
}
.flogo.asbtn:hover { background: rgb(0 0 0 / .05); }

.afform { display: flex; flex-direction: column; }
.afh {
  margin: 0 0 8px;
  font: 700 38px/1.06 var(--ui);
  color: var(--ink);
  letter-spacing: -.015em;
}
.afsub { margin: 0 0 22px; font: 400 15px/1.5 var(--ui); color: var(--ink-body); }
.aferr {
  margin: 0 0 16px; padding: 9px 12px;
  font: 400 13px/1.4 var(--ui);
  color: #7a0f10;
  background: #fdecec;
  border: 1px solid #f0b4b4;
  border-radius: var(--r-button);
}
.affield { display: block; margin-bottom: 14px; }
.affield span {
  display: block; margin-bottom: 5px;
  font: var(--type-meta); color: var(--ink-dim);
}
.affield input {
  display: block; width: 100%; height: 40px;
  padding: 0 12px;
  font: 400 15px/1 var(--ui);
  color: var(--ink);
  background: #ffffff;
  border: 1px solid var(--border-ctrl);
  border-radius: var(--r-button);
  box-shadow: inset 0 1px 2px rgb(0 0 0 / .07);
}
.affield input:focus {
  outline: none;
  border-color: var(--green-select);
  box-shadow: inset 0 1px 2px rgb(0 0 0 / .07), 0 0 0 3px rgb(var(--green-select-rgb) / .22);
}
.afbar { display: flex; align-items: center; gap: var(--sp-9); margin-top: 6px; }
.afnote {
  margin: 18px 0 0; max-width: 42ch;
  font: var(--type-secondary); color: var(--ink-caption);
}
.afnote:empty { display: none; }

.afstage { position: relative; aspect-ratio: 3 / 4; min-height: 480px; }
.afstage canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

@media (max-width: 860px) {
  .fauth { grid-template-columns: minmax(0, 1fr); gap: 20px; }
  .afstage { display: none; }
  .afh { font-size: 30px; }
}

/* ------------------------------------------------------------ when it breaks */

.ferr {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 420px;
  gap: 40px;
  align-items: center;
  max-width: 1000px;
  margin: 0 auto;
  padding: 60px 30px;
  min-height: 62vh;
}
.fetext h1 {
  margin: 0 0 12px;
  font: 700 38px/1.06 var(--ui);
  color: var(--ink);
  letter-spacing: -.015em;
}
/* The family is stated rather than inherited. #front sets `font:` on itself and
   a <p> inside a section picks up whatever an unlayered sheet last said, which
   here was a serif. */
.fetext p {
  margin: 0 0 22px; max-width: 40ch;
  font: 400 16px/1.5 var(--ui);
  color: var(--ink-body);
}
.feart { display: flex; justify-content: center; }
.feart img {
  max-width: 100%; height: auto; min-height: 340px; object-fit: contain;
  opacity: 0; transition: opacity 360ms cubic-bezier(.1,.9,.2,1);
}
.feart img.in { opacity: 1; }

/* ---------------------------------------------------------------- narrow - */

@media (max-width: 980px) {
  .fhero, .fshow, .fshow.flip, .fshow.props, .ferr { grid-template-columns: minmax(0, 1fr); }
  .fart.wide { max-width: 100%; aspect-ratio: 5 / 4; }
  .fshow.flip .fw, .fshow.flip .fart { order: 0; }
  .fstage { order: -1; min-height: 340px; max-width: 320px; margin: 0 auto; }
  .fart { max-width: 300px; margin: 0 auto; min-height: 330px; }
  .fh1 { font-size: 32px; }
  .fw h2 { font-size: 24px; }
  .fnav { display: none; }
  .ffly { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .frow img, .feart img { transition: none; }
}


/* --- the prerendered still under a cover ---------------------------------
   THE PANEL, ALREADY FINISHED, WHILE THE LIVE ONE WARMS UP.

   Each showcase stage was rendered once and captured (see PRERENDER in
   front.js). The still sits in the cover, filling it exactly as the canvas
   will, so the handover from picture to live scene is a change of source and
   not a change of layout.

   `contain`, not `cover`: these are figures on transparent ground and cropping
   one is cropping a person. The panel's own background shows through the
   margins, which is what it does behind the canvas too. */
.fcover .fpre {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity 260ms ease-out;
}
.fcover.haspre .fpre { opacity: 1; }
/* Once the still is up the spinner is answering a question nobody is asking. */
.fcover.haspre .fspin { display: none; }

/* The server-rendered shell paints before any script runs and is replaced by
   the real front page the moment it exists. Inert while it is up: its buttons
   have no handlers yet, and a control that looks live and does nothing is worse
   than one that plainly is not ready. */
#front-shell { pointer-events: none; }
