/* =========================================================================
 * Maegen Online + Azakhan Studios - reset and primitives.
 *
 * Everything here is shared by both sites. Nothing here knows which site it
 * is on; the difference is entirely in tokens.css's [data-theme] block.
 *
 * Import order matters:   fonts.css -> tokens.css -> base.css -> page.css
 * ========================================================================= */

/* --------------------------------------------------------------- reset -- */
*,*::before,*::after{ box-sizing:border-box }
html{ -webkit-text-size-adjust:100%; scroll-behavior:smooth }
@media (prefers-reduced-motion: reduce){ html{ scroll-behavior:auto } }

body{
  margin:0;
  background:var(--bg);
  color:var(--ink);
  font-family:var(--font-body);
  font-size:var(--step-body);
  line-height:var(--lh-body);
  letter-spacing:var(--ls-body);
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  overflow-x:hidden;          /* the page body must never scroll sideways */
}

h1,h2,h3,h4,p,figure,blockquote,dl,dd{ margin:0 }
ul[role="list"],ol[role="list"]{ list-style:none; margin:0; padding:0 }

/* Every image declares its own box. Zero CLS by construction, not by luck. */
img,svg,video,canvas{ display:block; max-width:100% }
img,video{ height:auto }

button,input,select,textarea{ font:inherit; color:inherit }

/* ------------------------------------------------------------- a11y ------
 * `outline:none` is FORBIDDEN on both sites, with no exception. The ring
 * geometry is shared; only its colour changes, because --rune on a --steel
 * control measures 1.03:1 and on parchment 1.29:1 - invisible both times.
 * The offset gap must expose the page ground, which is what isolates the
 * cyan from whatever is behind it. */
:where(a,button,[tabindex],summary,[role="button"]):focus-visible{
  outline:2px solid var(--focus-ring);
  outline-offset:2px;
  border-radius:var(--radius-1);
}
:where(a,button,[tabindex],summary):focus:not(:focus-visible){ outline:none }

.skip-link{
  position:absolute; left:var(--space-4); top:calc(var(--space-4) * -6);
  z-index:100; padding:var(--space-3) var(--space-5);
  background:var(--bg-raised); color:var(--ink-strong);
  border:1px solid var(--rule-frame); text-decoration:none;
  transition:top var(--dur-base) var(--ease-out);
}
.skip-link:focus{ top:var(--space-4) }

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

::selection{ background:color-mix(in srgb, var(--violet) 30%, transparent) }

/* --------------------------------------------------------------- links --
 * The accent law: cyan is a light source, never a paint. A link's TEXT stays
 * ink; only the underline lights up. That is also the correct accessibility
 * pattern - presence of an underline identifies the link, not its hue. */
a{ color:inherit; text-decoration:none }
.link{
  text-decoration:underline;
  text-decoration-color:var(--rule-frame);
  text-decoration-thickness:1px;
  text-underline-offset:.22em;
  transition:text-decoration-color var(--dur-base) var(--ease-standard);
}
.link:hover,.link:focus-visible{ text-decoration-color:var(--accent) }

/* ---------------------------------------------------------------- type -- */
.display-xl,.display-l,.display-m,.sub{
  font-family:var(--font-display);
  font-weight:600;
  color:var(--ink-heading);
  text-wrap:balance;
}
/* --step-display-xl IS USED EXACTLY ONCE ON THE ENTIRE GAME SITE, at the
 * technology beat: "Fifty milliseconds. Every time."  Holding the largest
 * size in reserve for two-thirds of the page is the whole reason that line
 * lands. This rule erodes silently - it is a review-checklist item, and CI
 * greps the built CSS for a second use. */
.display-xl{ font-size:var(--step-display-xl); line-height:var(--lh-display-xl); letter-spacing:var(--ls-display-xl) }
.display-l { font-size:var(--step-display-l);  line-height:var(--lh-display-l);  letter-spacing:var(--ls-display-l) }
.display-m { font-size:var(--step-display-m);  line-height:var(--lh-display-m);  letter-spacing:var(--ls-display-m) }
.sub       { font-size:var(--step-sub);        line-height:var(--lh-sub);        letter-spacing:var(--ls-sub); font-weight:300 }

.stat{
  font-family:var(--font-ui); font-weight:600;
  font-size:var(--step-stat); line-height:var(--lh-stat);
  letter-spacing:var(--ls-stat); font-variant-numeric:tabular-nums;
}

.prose{ max-width:var(--measure) }
.prose + .prose{ margin-top:var(--space-5) }

.caption{
  font-family:var(--font-ui); font-size:var(--step-caption);
  line-height:var(--lh-caption); color:var(--ink-quiet); letter-spacing:0;
}
/* Accent use 7: the figure NUMBER lights, never the clause after the dash.
 * --accent-sm, not --accent: a caption is 14px, and on the studio's light
 * ground the full accent measures 4.61:1 where the spec asks for the 6.47:1
 * variant below 17px. On night the two tokens are identical, so this costs
 * the game site nothing and fixes the studio site. */
.caption .fig{ color:var(--accent-sm) }

.micro{
  font-family:var(--font-ui); font-weight:500; font-size:var(--step-micro);
  letter-spacing:var(--ls-micro); text-transform:uppercase;
  color:var(--ink-quiet);
}

.mono{ font-family:var(--font-mono); font-size:var(--step-caption) }

/* -------------------------------------------------------------- eyebrow --
 * The cheapest piece of brand DNA available: a rule that fades out at its
 * outer end, the label, then a mirrored rule. That is the key art's ONLINE
 * between its two filigree flourishes, abstracted. It costs nothing, it
 * appears on every section of both sites, and it binds the pages to the logo
 * harder than any illustration would. */
.eyebrow{
  display:flex; align-items:center; gap:var(--space-4);
  font-family:var(--font-ui); font-weight:500;
  font-size:var(--step-label); letter-spacing:var(--ls-label);
  text-transform:uppercase; color:var(--ink-quiet);
}
.eyebrow::before,.eyebrow::after{
  content:""; flex:1 1 auto; height:1px; min-width:var(--space-5);
}
.eyebrow::before{ background:linear-gradient(to right,transparent,var(--rule-hair)) }
.eyebrow::after { background:linear-gradient(to left, transparent,var(--rule-hair)) }
/* A left-aligned eyebrow keeps only the trailing rule. */
.eyebrow--start::before{ display:none }
.eyebrow--start{ justify-content:flex-start }

/* ---------------------------------------------------------------- rules -- */
.rule      { border:0; height:1px; background:var(--rule-hair); margin:0 }
.rule--frame{ background:var(--rule-frame) }
@media (min-resolution:2dppx){
  /* At DPR>=2 a rule renders at half a pixel. That hairline crispness is
   * most of the "typeset" signal and it is free. */
  .rule{ height:.5px }
}

/* ---------------------------------------------------------------- plate --
 * A bounded rectangle in the grid, NOT the page's ground. That one decision
 * is why this direction survives a fast scroller, survives WebGL being off,
 * and wins the fill-rate budget: at DPR 2 a 519x652 plate is 1.35 Mpx against
 * a full-bleed 1440x900 canvas's 5.18 Mpx - 3.8x fewer pixels per frame,
 * straight out of the layout. */
.plate{
  position:relative;
  aspect-ratio:var(--plate-ratio);
  background:var(--bg-raised);      /* one step up from the page, so the frame
                                     * reads as a window and not as a hole */
  border:1px solid var(--rule-frame);
  isolation:isolate;
}
/* NO `overflow:hidden` HERE. The corner ticks sit 7px OUTSIDE the frame, and
 * clipping the plate erases all four of them - which is easy to miss, because
 * the plate still looks fine without them. The canvas and the still are both
 * position:absolute inset:0 with object-fit:cover, so there is nothing to
 * clip in the first place. */
/* A DESCENDANT selector, not a child one. The still is wrapped in <picture>
 * for the AVIF/WebP/JPEG negotiation, so `.plate > img` never matches it: the
 * image then lays out at its intrinsic aspect inside a portrait plate and
 * sits as a letterboxed band across the top. It looks like a design decision
 * rather than a bug, which is what makes it worth a comment. */
.plate :is(canvas, img){
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
}
.plate picture{ display:contents }
.plate canvas{ opacity:0; transition:opacity var(--dur-slow) var(--ease-out) }
.plate.is-live canvas{ opacity:1 }
.plate.is-live .plate-still{ opacity:0 }
.plate-still{ transition:opacity var(--dur-slow) var(--ease-out) }

/* Corner ticks: four 12px L-marks, 6px OUTSIDE each corner. A plate-mount
 * registration detail, and the cheapest "typeset with care" signal there is. */
.plate::before,.plate::after,
.plate-ticks::before,.plate-ticks::after{
  content:""; position:absolute; width:12px; height:12px; pointer-events:none;
  border:0 solid var(--rule-frame);
}
.plate::before{ top:-7px;    left:-7px;  border-top-width:1px; border-left-width:1px }
.plate::after { top:-7px;    right:-7px; border-top-width:1px; border-right-width:1px }
.plate-ticks::before{ bottom:-7px; left:-7px;  border-bottom-width:1px; border-left-width:1px }
.plate-ticks::after { bottom:-7px; right:-7px; border-bottom-width:1px; border-right-width:1px }
.plate-ticks{ position:absolute; inset:0; pointer-events:none }

/* AND THE BOTTOM TWO MARKS NEED SOMEWHERE TO BE.
 * A mark 7px outside the frame's bottom edge lands in the 7px under the frame,
 * and the caption was set flush there - measured at 1440x900, the bottom-left
 * L crossed the caption's own "F" on four of the five plates, 5 x 6px of
 * overlap each (`node verify/w7/ticks.mjs`). It is small and it is the kind of
 * thing that reads as a printing fault rather than as a detail, which is the
 * opposite of what the mark is for.
 *
 * 16px, and it is not a new number: `.captions{margin-top:var(--space-4)}` on
 * the game's systems plate already sets exactly this gap, and that one plate
 * was the only one measuring CLEAR - by 10px. So this makes the other four
 * agree with the one that was already right, rather than inventing a value.
 *
 * The alternative - trimming the marks so they stop at the corner - was
 * rejected: the 5px they carry past the corner is what makes them read as
 * registration marks rather than as a broken border, and it is the same
 * geometry on all four corners. The caption is the thing with room to move. */
.plate + .caption,
.plate + .captions{ margin-top:var(--space-4) }

/* The floor line and its reflected pool.
 *
 * DO NOT VALUE-ENGINEER THIS OUT. The reader's visual system resolves it in
 * under 200ms, before a word has been read, and it reports four things at
 * once: this is a room; this object has weight; someone placed it here on
 * purpose; there is a floor. The darkness stops being a background and
 * becomes a space. Inside a framed plate it does one more thing it could not
 * do anywhere else - a floor inside a frame is a diorama in a vitrine.
 *
 * It survives at EVERY tier, including tier 0, because it is CSS. */
.plate-floor{
  position:absolute; left:0; right:0; top:var(--floor-line);
  height:1px; pointer-events:none;
  background:linear-gradient(to right,
    transparent 0%, color-mix(in srgb,var(--steel) 14%,transparent) 25%,
    color-mix(in srgb,var(--steel) 14%,transparent) 75%, transparent 100%);
}
.plate-pool{
  position:absolute; left:50%; top:var(--floor-line);
  width:35%; height:4%; transform:translate(-50%,-50%);
  pointer-events:none; border-radius:50%;
  background:radial-gradient(ellipse at center,
    color-mix(in srgb,var(--rune-core) 20%,transparent) 0%,
    transparent 70%);
}

/* -------------------------------------------------------------- actions -- */
.btn{
  display:inline-flex; align-items:center; justify-content:center;
  gap:var(--space-3); padding:var(--space-4) var(--space-6);
  min-height:48px;
  font-family:var(--font-ui); font-weight:500; font-size:var(--step-label);
  letter-spacing:var(--ls-label); text-transform:uppercase;
  color:var(--ink-strong); background:transparent;
  border:1px solid var(--rule-frame); border-radius:var(--radius-1);
  cursor:pointer;
  transition:border-color var(--dur-base) var(--ease-standard),
             color var(--dur-base) var(--ease-standard);
}
/* A button is steel with a cyan RIM, never a cyan button. If you cannot name
 * where the light is arriving from, delete the cyan pixel. */
.btn:hover,.btn:focus-visible{ border-color:var(--accent); color:var(--ink-strong) }
.btn[aria-disabled="true"]{ opacity:.5; cursor:not-allowed }

.btn--quiet{
  border:0; padding-inline:0; min-height:auto;
  color:var(--ink-quiet);
  text-decoration:underline; text-decoration-color:transparent;
  text-underline-offset:.4em; text-decoration-thickness:1px;
  transition:text-decoration-color var(--dur-base) var(--ease-standard),
             color var(--dur-base) var(--ease-standard);
}
.btn--quiet:hover,.btn--quiet:focus-visible{
  color:var(--ink); text-decoration-color:var(--accent);
}

/* ------------------------------------------------------- a switch's state --
 * The word after "MOTION". Both words are in the markup and this picks one -
 * the same rule the plate captions follow, and for the same two reasons.
 *
 * 1. NO SCRIPT WRITES TEXT INTO A CONTROL. The word is a function of
 *    aria-pressed, so it cannot drift out of step with the attribute a screen
 *    reader is reading, and with scripting off the document already says the
 *    true thing instead of waiting to be corrected.
 * 2. THE BOX IS AS WIDE AS THE WIDER WORD. Both words occupy the same grid
 *    cell, so an inline-grid sized by its content is "OFF" wide in every font
 *    and at every tracking. That is what stops the switch - and the font swap
 *    underneath it - from moving the header bar. `display:none` on the unused
 *    word would collapse it back to the narrower one and hand the shift
 *    straight back; `visibility:hidden` keeps the cell and is also skipped by
 *    find-in-page and by the accessibility tree, so nothing offers a reader a
 *    word that is not on the screen. */
.switch-state{ display:inline-grid; vertical-align:baseline }
.switch-state > span{ grid-area:1 / 1; visibility:hidden }
[aria-pressed="true"] .switch-state__on,
[aria-pressed="false"] .switch-state__off{ visibility:visible }

/* --------------------------------------------------------------- reveal --
 * Content arrives a beat after the section does, staggered per element, ONCE.
 * The observer unobserves inside its own callback, so it fires once by
 * CONSTRUCTION rather than once by convention - that is what stops it
 * re-triggering when the reader scrolls back up.
 * 12px of travel, not 40: large type moving 40px reads as a slide deck. */
.reveal > *{
  opacity:0; transform:translateY(12px);
  transition:opacity var(--dur-slow) var(--ease-out),
             transform var(--dur-slow) var(--ease-out);
  transition-delay:calc(var(--i,0) * var(--stagger));
}
.reveal.is-in > *{ opacity:1; transform:none }
@media (prefers-reduced-motion: reduce){
  .reveal > *{ opacity:1; transform:none; transition:none }
}
[data-motion="off"] .reveal > *{ opacity:1; transform:none; transition:none }

/* ----------------------------------------------------------------- grid --
 * Two layouts, and which one a section uses is a stated decision, not a
 * habit. A: symmetrical, for title pages. B: asymmetrical, for reading. */
/* --content is the CONTENT width, not the border-box width: at 1440 the
 * margins must land at x=80 and x=1360, giving 1280px of content and a
 * 84.67px column. Setting max-width to --content alone makes the padding eat
 * into it, the column comes out at 71.5px, and the hero plate lands at 453px
 * instead of the spec'd 519 - which quietly breaks the "plate is never
 * narrower than 30% of the viewport" kill criterion at exactly the width
 * nobody re-measures. */
.wrap{
  width:100%; margin-inline:auto;
  max-width:calc(var(--content) + var(--page-pad) * 2);
  padding-inline:var(--page-pad);
}
.layout-a,.layout-b{
  display:grid; gap:var(--gutter); align-items:start;
}
/* A grid item defaults to `min-width:auto`, which means it refuses to shrink
 * below its content's minimum. One wide child - a diagram with a min-width, a
 * code block, a table - therefore does not overflow ITSELF, it pushes its
 * whole track wider than the viewport and the BODY scrolls sideways. The
 * `overflow-x:auto` scroller you carefully put around that child never even
 * gets the chance to scroll, because it was granted all the room it asked for.
 *
 * Measured before this line existed: at a 390px viewport the document was
 * 621px wide. It is the single most common way a careful responsive layout
 * breaks, and it breaks quietly, because everything looks right until the one
 * page that has a wide figure on it. */
.layout-a > *,.layout-b > *{ min-width:0 }
.layout-a{ grid-template-columns:repeat(12,minmax(0,1fr)) }
.layout-a > .rail  { grid-column:1 / span 2 }
.layout-a > .main  { grid-column:3 / span 5 }
.layout-a > .plate-col{ grid-column:8 / span 5 }

.layout-b{ grid-template-columns:repeat(12,minmax(0,1fr)) }
.layout-b > .rail  { grid-column:1 / span 2 }
.layout-b > .main  { grid-column:3 / span 7 }
.layout-b > .margin{ grid-column:10 / span 3 }
/* The ~204px of air between the end of the 62ch measure and the margin gutter
 * IS NOT WASTE. It is what makes the column look set rather than stretched.
 * Do not fill it. */

.rail{
  font-family:var(--font-ui); font-weight:300;
  color:color-mix(in srgb,var(--steel) 30%,transparent);
}
.rail .numeral{
  font-family:var(--font-display); font-weight:300;
  font-size:var(--step-display-m); line-height:1;
}

/* Mobile: the plate moves above the text and turns landscape; the rail
 * numeral becomes an inline prefix; margin notes collapse to <details>. */
@media (max-width:899px){
  .layout-a,.layout-b{ grid-template-columns:minmax(0,1fr) }
  .layout-a > .rail,.layout-b > .rail{ grid-column:1 }
  .layout-a > .main,.layout-b > .main,
  .layout-a > .plate-col,.layout-b > .margin{ grid-column:1 }
  .layout-a > .plate-col{ order:-1 }
  .plate{ aspect-ratio:4/3 }
  .rail{ display:flex; align-items:baseline; gap:var(--space-3) }
  .rail .numeral{ font-size:var(--step-label) }
  .rail .numeral::after{ content:" \00b7" }
}

/* -------------------------------------------------------------- rhythm -- */
.section{ padding-block:var(--section) }
.chapter{ padding-block:var(--chapter) }
/* Exactly THREE 192px chapter breaks on the game site and only three: after
 * the hero, around the full-bleed valley, and before the colophon. */

/* --------------------------------------------------------- margin notes --
 * Below 900px the margin column has nowhere to go, and the usual fix -
 * dumping it inline - destroys the reading rhythm this direction is built on.
 * A <details> keyed by its figure number keeps the note available without
 * interrupting the measure. This is real work, budgeted here rather than
 * discovered late. */
.note{
  font-family:var(--font-ui); font-size:var(--step-caption);
  line-height:var(--lh-caption); color:var(--ink-quiet);
  border-top:1px solid var(--rule-hair); padding-top:var(--space-3);
}
.note > summary{ cursor:pointer; list-style:none; color:var(--accent-sm) }
.note > summary::-webkit-details-marker{ display:none }
/* MARKUP CONTRACT: write the element as `<details class="note" open>`. The
 * `open` is not optional and it is not a default anyone can safely drop.
 *
 * A closed <details> does not hide its children with `display:none` on the
 * children - it hides the SLOT they are projected into, inside the element's
 * shadow tree. Author CSS in the light DOM cannot reach a slot, so the
 * obvious desktop rule (`.note > * { display:block }`) silently does nothing
 * and the note renders as a hairline with no text under it. That is a
 * particularly nasty failure because the border-top still draws: the page
 * looks like it has a rule where a note should be, which reads as a spacing
 * bug rather than as missing content, and it survives review.
 *
 * `::details-content` is the pseudo-element that actually addresses the slot,
 * and it is the correct mechanism - but it is recent enough that it cannot be
 * the only thing holding the note open. So: `open` in the markup carries it
 * everywhere, and the rule below carries a note that lost its attribute.
 */
@media (min-width:900px){
  .note > summary{ display:none }
  .note::details-content{ content-visibility:visible; block-size:auto }
}
/* Below 900px the summary comes back and the note is collapsible by the
 * reader - the margin column has nowhere to go there, and dumping it inline
 * is what destroys the reading rhythm this direction is built on. */
@media (max-width:899px){
  .note > summary{ display:list-item }
}

/* ---------------------------------------------------------- full bleed --
 * The key art gets the SINGLE full-bleed moment on the entire site. Nothing
 * else ever bleeds - which is exactly why that one moment lands. */
.bleed{
  width:100vw; margin-inline:calc(50% - 50vw);
  height:100svh; position:relative; overflow:hidden;
}
.bleed img{ width:100%; height:100%; object-fit:cover }
.bleed figcaption{
  position:absolute; left:var(--page-pad); bottom:var(--page-pad);
}
