/* XNGMXR — void + wireframe horizon */

:root{
  --red:        #ff0a2e;
  /* The live signal colour, kept as components so glows can take their own
     alpha from it — rgb(var(--accent-rgb) / .8). A colour cannot be taken
     apart in CSS, and every rune here carries a drop-shadow, so storing the
     resolved colour alone would leave blue glyphs haloed in red.

     Defaults to the red. Only the landing page ever retunes it, as you point
     at a project; everything else falls through. */
  --accent-rgb: 255 10 46;
  --accent:     rgb(var(--accent-rgb));
  --red-soft:   #ff5566;
  --red-deep:   #8a0016;
  --white:      #e9e9ee;
  --dim:        #6b6b76;
  --bg:         #000000;
  --mono: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;
}

*{ box-sizing: border-box; }

html, body{
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--white);
  font-family: var(--mono);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

body{ overscroll-behavior: none; }

/* ---------- background layers ---------- */

/* inlined glyph definitions — never rendered directly */
.sigil-sprite{
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

#vista{
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
}

/* faint drifting grain — keeps the black from reading as flat #000 */
.grain{
  position: fixed;
  /* was -50%, which made this layer four times the viewport in area. The
     drift only translates by 3%, so a small bleed is all it ever needed. */
  inset: -6%;
  z-index: 1;
  pointer-events: none;
  opacity: .035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: grainDrift 8s steps(6) infinite;
  will-change: transform;
}

@keyframes grainDrift{
  0%   { transform: translate(0,0); }
  20%  { transform: translate(-3%, 2%); }
  40%  { transform: translate(2%, -3%); }
  60%  { transform: translate(-2%, -2%); }
  80%  { transform: translate(3%, 1%); }
  100% { transform: translate(0,0); }
}

.vignette{
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    radial-gradient(120% 90% at 50% 48%, transparent 40%, rgba(0,0,0,.55) 80%, #000 100%),
    linear-gradient(#000 0%, transparent 18%, transparent 82%, rgba(0,0,0,.85) 100%);
}

/* ---------- layout ---------- */

.shell{
  position: relative;
  z-index: 3;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  padding: clamp(1.6rem, 4vh, 3.2rem) 1.5rem clamp(1.4rem, 3.5vh, 2.6rem);
  text-align: center;
}

/* ---------- crown: sigil ---------- */

.crown{
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(.9rem, 2.2vh, 1.5rem);
}

.sigil{
  width: min(74vw, 480px);
  height: auto;
  color: var(--accent);
  /* two passes, not three: this element animates its opacity, so every
     extra filter is another full pass re-run on each frame */
  filter:
    drop-shadow(0 0 5px rgb(var(--accent-rgb) / .85))
    drop-shadow(0 0 22px rgb(var(--accent-rgb) / .4));
  animation: sigilBreathe 9s ease-in-out infinite;
}

@keyframes sigilBreathe{
  0%, 100% { opacity: .92; }
  50%      { opacity: 1; }
}

/* the sigil spells it; this says it */
.wordmark{
  margin: 0;
  font-size: clamp(.68rem, 2.1vw, .95rem);
  font-weight: 400;
  letter-spacing: 1.05em;
  text-indent: 1.05em;   /* offsets the trailing letter-space so it stays centred */
  text-transform: uppercase;
  color: var(--white);
  opacity: .82;
  text-shadow: 0 0 12px rgb(var(--accent-rgb) / .35), 0 0 30px rgb(var(--accent-rgb) / .18);
}

/* ---------- index / project menu ---------- */

.index{
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .9rem;
}

.index-head{
  margin: 0;
  font-size: .55rem;
  font-weight: 400;
  letter-spacing: .5em;
  text-indent: .5em;
  text-transform: uppercase;
  color: var(--dim);
}

.index-head::before,
.index-head::after{
  content: "";
  display: inline-block;
  width: 1.4rem;
  height: 1px;
  vertical-align: .28em;
  background: currentColor;
  opacity: .5;
}
.index-head::before{ margin-right: .9rem; }
.index-head::after{ margin-left: .3rem; }

.index ul{
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: .45rem;
}

/* The three doors, abreast and in written order: ritual left, writing
   centre, collaboration right. They do not move when one is chosen — a menu
   that rearranges itself costs you the ability to know where a thing is
   before you look for it. Wraps to a column when there is not room for
   three; on a phone the row would crush the letter-spacing that makes these
   read as inscriptions rather than buttons. */
.index ul.doors{
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: clamp(.5rem, 3.5vw, 2.4rem);
  margin-bottom: .3rem;
}

@media (max-width: 34rem){
  .index ul.doors{ flex-direction: column; gap: .35rem; }
}

/* A door is an entry with more weight — it names a world rather than a
   thing inside one. The chosen one holds the accent it set. */
.door{
  font-size: clamp(.8rem, 2.5vw, 1.05rem);
  letter-spacing: .34em;
  opacity: .5;
  transition: color .25s, opacity .3s, letter-spacing .35s;
}

.door.is-current{
  opacity: 1;
  color: var(--accent);
  text-shadow: 0 0 10px rgb(var(--accent-rgb) / .6), 0 0 28px rgb(var(--accent-rgb) / .3);
}

.door:hover, .door:focus-visible{ opacity: 1; }

/* an empty domain says so rather than showing a blank gap */
.items .empty{
  font-size: .62rem;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--dim);
  opacity: .55;
  padding: .45rem .9rem;
}

.entry{
  position: relative;
  display: inline-flex;
  text-decoration: none;
  align-items: baseline;
  gap: .8rem;
  padding: .45rem .9rem;
  border: 1px solid transparent;
  background: none;
  font: inherit;
  font-size: clamp(.72rem, 2.1vw, .88rem);
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--white);
  cursor: pointer;
  transition: color .25s, border-color .25s, letter-spacing .35s;
}

.entry .num{
  font-size: .6em;
  letter-spacing: .12em;
  color: var(--accent);
  opacity: .75;
}

.entry::before{
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--accent);
  opacity: 0;
  transform: scale(1.08);
  transition: opacity .25s, transform .25s;
  pointer-events: none;
}

.entry:hover,
.entry:focus-visible{
  color: #fff;
  letter-spacing: .3em;
  outline: none;
  text-shadow: 0 0 10px rgb(var(--accent-rgb) / .75), 0 0 26px rgb(var(--accent-rgb) / .35);
}

.entry:hover::before,
.entry:focus-visible::before{
  opacity: .55;
  transform: scale(1);
}

.entry[aria-expanded="true"]{ color: var(--red-soft); }

/* --- word -> sigil swap ---------------------------------------------
   Word and glyphs share one grid cell, so the cell is as wide as the
   wider of the two and nothing reflows when they trade places. */

.swap{
  display: inline-grid;
  grid-template-areas: "cell";
  align-items: center;
  justify-items: center;
}

.swap > *{ grid-area: cell; }

.swap-word{
  transition: opacity .22s ease, filter .22s ease;
}

.swap-sigil{
  height: 1.95em;
  width: auto;
  color: var(--accent);
  opacity: 0;
  transform: scale(.92);
  pointer-events: none;
  filter:
    drop-shadow(0 0 4px rgb(var(--accent-rgb) / .8))
    drop-shadow(0 0 14px rgb(var(--accent-rgb) / .4));
  transition: opacity .22s ease, transform .3s cubic-bezier(.2,.7,.3,1);
}

.entry:hover .has-sigil .swap-word,
.entry:focus-visible .has-sigil .swap-word{
  opacity: 0;
  filter: blur(2px);
}

.entry:hover .has-sigil .swap-sigil,
.entry:focus-visible .has-sigil .swap-sigil{
  opacity: 1;
  transform: scale(1);
}

/* ---------- project panel ---------- */

.panel{
  position: fixed;
  inset: 0;
  z-index: 10;
  display: grid;
  place-items: center;
  padding: 2rem 1.5rem;
  background: rgba(0,0,0,.82);
  backdrop-filter: blur(6px);
  animation: panelIn .35s ease both;
}

.panel[hidden]{ display: none; }

@keyframes panelIn{
  from { opacity: 0; }
  to   { opacity: 1; }
}

.panel-inner{
  position: relative;
  width: min(52rem, 100%);
  max-height: 80vh;
  overflow-y: auto;
  padding: clamp(2rem, 6vw, 3.5rem);
  border: 1px solid rgb(var(--accent-rgb) / .35);
  background: rgba(0,0,0,.72);
  box-shadow: 0 0 60px rgb(var(--accent-rgb) / .12), inset 0 0 80px rgba(0,0,0,.9);
  text-align: left;
}

.panel-title{
  margin: 0 0 1.4rem;
  font-size: clamp(1.05rem, 3.6vw, 1.7rem);
  font-weight: 400;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--white);
  text-shadow: 0 0 18px rgb(var(--accent-rgb) / .4);
}

.panel-title::after{
  content: "";
  display: block;
  width: 100%;
  height: 1px;
  margin-top: 1.2rem;
  background: linear-gradient(90deg, var(--accent), rgb(var(--accent-rgb) / 0));
}

.panel-body{
  font-size: .78rem;
  line-height: 2;
  letter-spacing: .1em;
  color: var(--dim);
}

.panel-close{
  position: absolute;
  top: .9rem;
  right: 1rem;
  padding: .4rem .6rem;
  border: 1px solid transparent;
  background: none;
  font: inherit;
  font-size: .8rem;
  color: var(--dim);
  cursor: pointer;
  transition: color .2s, border-color .2s;
}

.panel-close:hover,
.panel-close:focus-visible{
  color: var(--accent);
  border-color: rgb(var(--accent-rgb) / .4);
  outline: none;
}

/* ---------- short viewports ---------- */

@media (max-height: 620px){
  .sigil{ width: min(60vw, 340px); }
  .wordmark{ letter-spacing: .75em; text-indent: .75em; }
}

@media (prefers-reduced-motion: reduce){
  .grain, .sigil{ animation: none; }
  .panel{ animation: none; }
  .swap-word, .swap-sigil{ transition-duration: .01ms; }
  .swap-sigil{ transform: none; }
}

/* ======================================================================
   ALBUM PAGE — praeordinatus.html
   Same crown geometry as the index, so the sigil sits in the same place.
   ====================================================================== */

.album-page{ overflow-x: hidden; }

.scanlines{
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: .5;
  background: repeating-linear-gradient(
    to bottom,
    rgba(255,10,46,.035) 0px,
    rgba(255,10,46,.035) 1px,
    transparent 1px,
    transparent 3px);
}

.album{
  position: relative;
  z-index: 3;
  min-height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  gap: clamp(.8rem, 2.2vh, 1.6rem);
  padding: clamp(1.6rem, 4vh, 3.2rem) 1.5rem clamp(1.4rem, 3.5vh, 2.6rem);
  text-align: center;
}

.home{ display: block; color: var(--red); }
.home:focus-visible{ outline: none; }
.home:hover .sigil,
.home:focus-visible .sigil{ opacity: 1; }

.album-title{
  margin: 0;
  font-size: clamp(.58rem, 1.7vw, .74rem);
  font-weight: 400;
  letter-spacing: .5em;
  text-indent: .5em;
  text-transform: uppercase;
  color: var(--white);
  opacity: .7;
}

.sep{ color: var(--red); }

/* ---------- numogram ---------- */

/* the figure is its own layer filling the page; crown and deck float over
   it. Both are click-through except for their own controls, so a zone
   drifting behind the sigil is still reachable. */
.stage{
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.numogram, .node{ pointer-events: auto; }

.crown, .deck{ position: relative; z-index: 2; }
.crown{ pointer-events: none; }
.home, .track-name, .player{ pointer-events: auto; }

/* driven from the loop: dips as a zone passes behind the mark */
.crown{
  opacity: var(--crown-op, 1);
  transition: opacity .18s linear;
}

/* keyboard focus is deliberately left alone — this is about not clicking
   something you can no longer see */
.crown.is-veiled .home{ pointer-events: none; }

.numogram-wrap{
  position: relative;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* the camera drift is written here rather than onto an SVG group, so the
     rotation and scale are a compositor job instead of a full re-raster of
     the figure on every frame */
  transform-origin: 50% 50%;
  will-change: transform;
}

.numogram{
  position: relative;
  width: min(94vh, 94vw);
  height: auto;
  overflow: visible;
  transition: opacity .5s ease;
}

.numogram.is-idle{ opacity: .72; }

/* an invisible, generous target — the zones are orbiting now */
.node-hit{ fill: transparent; }

.current-path{
  fill: none;
  stroke: var(--red);
  stroke-width: 1;
  opacity: .16;
  transition: opacity .45s ease, stroke-width .45s ease;
}

.current-path.is-live{
  opacity: .85;
  stroke-width: 1.6;
  filter: drop-shadow(0 0 6px rgba(255,10,46,.6));
}

.current-path.is-anchor{ opacity: .45; }

/* a channel between two zones that have drifted together: fine dashes
   running the gap, brightening as they close */
.link{
  /* no drop-shadow here on purpose: ten of these animate at once, and a
     filter on each forces ten offscreen passes every frame. A brighter,
     slightly heavier stroke reads the same and costs nothing. */
  stroke: var(--red-soft);
  stroke-width: 1.4;
  stroke-dasharray: 1.5 5;
  stroke-linecap: round;
  pointer-events: none;
  animation: dataRun .9s linear infinite;
}

/* -32.5 is five whole dash periods, so the run is seamless */
@keyframes dataRun{ to { stroke-dashoffset: -32.5; } }

.spoke{
  stroke: var(--red);
  stroke-width: 1;
  opacity: .10;
  stroke-dasharray: 3 6;
  transition: opacity .45s ease;
}

.spoke.is-live{ opacity: .6; }

.node{ cursor: pointer; }

.node-halo{
  fill: none;
  stroke: var(--red);
  stroke-width: 1;
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
  transition: opacity .35s ease, transform .35s ease;
}

.node-disc{
  fill: #000;
  stroke: var(--red);
  stroke-width: 1.2;
  opacity: .5;
  transition: fill .35s ease, opacity .35s ease, stroke-width .35s ease;
}

.node:hover .node-disc,
.node:focus-visible .node-disc{ opacity: .9; }
.node:focus-visible{ outline: none; }
.node:focus-visible .node-halo{ opacity: .5; }

.node.is-active .node-disc{
  fill: var(--red);
  opacity: 1;
  stroke-width: 1.6;
  filter: drop-shadow(0 0 10px rgba(255,10,46,.8));
}

.node.is-active .node-halo{ opacity: .7; }

.node.is-twin .node-disc{
  opacity: 1;
  stroke-width: 1.6;
  filter: drop-shadow(0 0 7px rgba(255,10,46,.55));
}

.node.is-twin .node-halo{
  opacity: .35;
  animation: twinPulse 2.4s ease-in-out infinite;
}

@keyframes twinPulse{
  0%, 100% { opacity: .18; transform: scale(.92); }
  50%      { opacity: .45; transform: scale(1.04); }
}

/* conjunction: all four zones back on their cardinal axes */
.numogram.is-locked .current-path{
  opacity: 1;
  stroke-width: 2;
  filter: drop-shadow(0 0 12px rgba(255,10,46,.95));
}

.numogram.is-locked .dial{ opacity: .7; }
.numogram.is-locked .node-disc{ stroke-width: 2; opacity: 1; }

/* ---------- per-track visualisers ----------
   The canvas is composited with screen, so black on it is transparent and
   the figure reads through: this layer only ever adds light. Everything
   that has to go dark is one of the state classes below, which is a style
   change on an element rather than a paint.

   Placed after .is-active, .is-twin and .is-locked on purpose — these carry
   the same specificity, so source order is what decides, and a converted or
   cursed zone has to win against being the selected one. */

/* The outer field. Fixed to the viewport rather than to the figure, because
   the space it exists to use is the margin the figure does not occupy. Sits
   under .stage (z-index 1) and the crown and deck (2), and the circle is
   punched out of its own pixels, so it can never crowd the numogram however
   loud it gets. */
.field{
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  z-index: 0;
  opacity: 0;
  transition: opacity .7s ease;
}

.field.is-live{ opacity: 1; }

/* Fixed to the viewport, not to the figure, so dust shed by a zone can drift
   off the diagram and across the page. It sits after .stage in the document
   at the same z-index, so it paints above the numogram and below the crown
   and deck. The camera drift is folded into the canvas transform instead of
   being inherited from .numogram-wrap. */
.viz{
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  z-index: 1;
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity .55s ease;
}

/* only while the track is playing */
.viz.is-live{ opacity: 1; }

/* assimilated: drained of red. White is the only other colour on this site,
   and a zone that has stopped being part of the red system is exactly what
   the front leaves behind. */
.node.is-assimilated .node-disc{
  fill: var(--white);
  stroke: var(--white);
  stroke-width: 1.4;
  opacity: 1;
  filter: none;
}

.node.is-assimilated .node-halo,
.node.is-assimilated .node-ping{ opacity: 0; animation: none; }

/* and it stops trailing ghosts — the history of a converted zone goes with
   it. A CSS opacity beats the attribute the loop writes, so nothing has to
   be coordinated with the frame. */
.numogram.is-taken-0 .echo-n0,
.numogram.is-taken-1 .echo-n1,
.numogram.is-taken-3 .echo-n3,
.numogram.is-taken-4 .echo-n4{ opacity: 0; }

/* cursed: a hole punched in the figure. This is the node whose own track is
   playing, so it has .is-active on it too and has to override the lit
   state — the point being that the one dark thing on the diagram is the
   zone the music belongs to. */
.node.is-cursed .node-disc{
  fill: #000;
  stroke: var(--red-deep);
  stroke-width: 1.4;
  opacity: 1;
  filter: none;
}

.node.is-cursed .node-halo,
.node.is-cursed .node-ping{ opacity: 0; animation: none; }

/* the name bound round the cursed node; opacity is driven per frame */
.curse-ring{
  fill: var(--red-soft);
  pointer-events: none;
}

/* ---------- numogram motion ----------
   Everything turns at a different rate: the dial drifts, the live current
   runs a bead round it, the spokes bleed inward toward zone 2, and the
   selected node pings. Slow enough to read as ritual rather than UI. */

.dial{
  transform-box: view-box;
  transform-origin: 50% 50%;
}

.dial{
  fill: none;
  stroke: var(--red);
  stroke-width: 1;
  opacity: .26;
}

/* turning against each other at unrelated rates, so the beat pattern
   between their mismatched tick counts never settles */
.dial-a{ animation: dialDrift 120s linear infinite; will-change: transform; }
.dial-b{ animation: dialDrift  96s linear infinite reverse; opacity: .2; will-change: transform; }

@keyframes dialDrift{ to { transform: rotate(360deg); } }

/* Latin circling the rim, the two bands turning against each other */
.rim{
  transform-box: view-box;
  transform-origin: 50% 50%;
}

/* natural spacing, not squeezed to fit — the band scrolls instead, so the
   words keep their proper rhythm and the rest is simply not rendered */
/* The veil holds the fade still in screen space; the band turns underneath.
   Two elements, because a mask on the rotating element would turn with it and
   the fade has to stay put over the sigil and the cipher. */
.rim-veil{
  position: absolute;
  inset: 0;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0%, transparent 13%, #000 26%, #000 79%, transparent 90%, transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0%, transparent 13%, #000 26%, #000 79%, transparent 90%, transparent 100%);
}

.rim-spin{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
  transform-box: view-box;
  transform-origin: 50% 50%;
  will-change: transform;
}

/* counter-turning, and at unrelated periods so they never settle into step */
.rim-spin-inner{ animation: dialDrift 260s linear infinite; }
.rim-spin-outer{ animation: dialDrift 190s linear infinite reverse; }

.rim-text{
  fill: var(--red);
  font-family: var(--mono);
  font-size: 8px;
  letter-spacing: 1.9px;
}

.rim-titles .rim-text{ opacity: .42; }

/* the outer band: glyphs, not letters */
.rim-glyphs{
  fill: var(--red);
  opacity: .38;
}

/* one tick per word break, read straight out of the strip's own spacing */
.rim-divider{
  stroke: var(--red);
  stroke-width: 1;
  opacity: .55;
}

/* doubled, so the start and end of the strip is distinguishable from a break */
.rim-seam{
  stroke: var(--red-soft);
  stroke-width: 1.3;
  opacity: .9;
}

/* where the zones were a bar ago, two bars, three */
.echo-dot{
  fill: none;
  stroke: var(--red);
  stroke-width: 1;
  pointer-events: none;
}

.current-flow{
  fill: none;
  stroke: var(--red);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 2 34;
  opacity: 0;
  transition: opacity .5s ease;
}

.current-flow.is-live{
  opacity: .9;
  filter: drop-shadow(0 0 6px rgba(255,10,46,.9));
  animation: flowRun 9s linear infinite;
}

.current-flow.flow-reverse.is-live{
  animation-duration: 6s;
  animation-direction: reverse;
}

/* -360 is ten whole dash periods (2 + 34), so the loop is seamless. Beads
   ride the path itself now, which a rotation could not do once the orbits
   stopped being circles. */
@keyframes flowRun{ to { stroke-dashoffset: -360; } }

/* dashes drift from each zone down its spoke into the centre */
.spoke.is-live{ animation: spokeBleed 2.2s linear infinite; }

@keyframes spokeBleed{ to { stroke-dashoffset: -9; } }

.node-ping{
  fill: none;
  stroke: var(--red);
  stroke-width: 1;
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
}

.node.is-active .node-ping{ animation: ping 3.4s ease-out infinite; }

@keyframes ping{
  0%   { opacity: .55; transform: scale(1); }
  70%  { opacity: 0;   transform: scale(2.3); }
  100% { opacity: 0;   transform: scale(2.3); }
}

/* ---------- deck: name, cipher, transport ---------- */

.deck{
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(.5rem, 1.4vh, .9rem);
}

.track-name{
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: .65em;
  /* held steady so switching tracks never nudges the transport: track 1 has
     no glyph strip to set the height, and the strips differ wildly in width,
     so cap both rather than let each track size its own line */
  min-height: 2.4em;
  font-size: clamp(.74rem, 2.2vw, .98rem);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--white);
  cursor: default;
}

/* The strips run from 3.3:1 to 15:1, so an uncapped one blew its cell out to
   445px around a 185px word. Capping the width letterboxes the longest strip
   instead of stranding the text in dead space. */
.track-name .swap-sigil{
  height: 1.8em;
  max-width: min(17em, 46vw);
}

/* English reads as English until hovered, then rewrites into glyphs */
.track-name:hover .has-sigil .swap-word,
.track-name:focus-within .has-sigil .swap-word{
  opacity: 0;
  filter: blur(2px);
}

.track-name:hover .has-sigil .swap-sigil,
.track-name:focus-within .has-sigil .swap-sigil{
  opacity: 1;
  transform: scale(1);
}

/* Untranslated sigilskript, subtitling the title above. Height is reserved
   whether or not a file exists, so the transport never moves. */
.cipher{
  margin: 0;
  min-height: 2em;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(.74rem, 2.2vw, .98rem);
}

.cipher-mark{
  /* the cipher strips run 17:1 to 25:1, so a tight cap made every one render
     at the same width and therefore a different glyph size. Sized by height
     instead, so the glyphs match across tracks and only the line length
     varies - the way a line of text behaves. */
  height: 1.25em;
  width: auto;
  max-width: min(38em, 88vw);
  color: var(--red);
  opacity: 0;
  transform: scale(.96);
  filter: drop-shadow(0 0 5px rgba(255,10,46,.55));
  transition: opacity .5s ease, transform .55s cubic-bezier(.2,.7,.3,1);
}

.cipher.has-mark .cipher-mark{ opacity: .72; transform: scale(1); }

/* an empty <svg> falls back to 300x150, which blew the deck wide open before
   this — keep it collapsed until it actually holds a mark */
.cipher:not(.has-mark) .cipher-mark{ width: 0; }

/* ---------- transport ---------- */

.player{
  display: flex;
  align-items: center;
  gap: clamp(.7rem, 2vw, 1.1rem);
  width: min(34rem, 92vw);
  transition: opacity .3s;
}

.player.is-idle{ opacity: .38; }

.play{
  flex: none;
  width: 2.1rem; height: 2.1rem;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid rgba(255,10,46,.5);
  border-radius: 50%;
  background: none;
  color: var(--red);
  cursor: pointer;
  transition: background .25s, box-shadow .25s, color .25s;
}

.play svg{ width: 1.1rem; height: 1.1rem; fill: currentColor; }
.play .icon-pause{ display: none; }
.play.is-playing .icon-play{ display: none; }
.play.is-playing .icon-pause{ display: inline; }

.play:hover:not(:disabled),
.play:focus-visible:not(:disabled){
  background: rgba(255,10,46,.12);
  box-shadow: 0 0 14px rgba(255,10,46,.4);
  outline: none;
}

.play:disabled{ cursor: default; }

.seek{
  --p: 0;
  flex: 1;
  min-width: 0;
  height: 2px;
  appearance: none;
  -webkit-appearance: none;
  background: linear-gradient(to right,
    var(--red) calc(var(--p) * 1%),
    rgba(255,10,46,.20) calc(var(--p) * 1%));
  cursor: pointer;
}

.seek:focus-visible{ outline: 1px solid rgba(255,10,46,.5); outline-offset: 6px; }
.seek:disabled{ cursor: default; }

.seek::-webkit-slider-thumb{
  -webkit-appearance: none;
  width: 9px; height: 9px;
  border: none;
  border-radius: 50%;
  background: var(--red);
  box-shadow: 0 0 8px rgba(255,10,46,.8);
}

.seek::-moz-range-thumb{
  width: 9px; height: 9px;
  border: none;
  border-radius: 50%;
  background: var(--red);
  box-shadow: 0 0 8px rgba(255,10,46,.8);
}

.time{
  flex: none;
  font-size: .56rem;
  letter-spacing: .14em;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 60rem){
  .numogram{ width: min(88vh, 96vw); }
}

/* narrow enough that the longer titles wrap to two lines — reserve for the
   worst case so the name block does not ride up and down between tracks */
@media (max-width: 30rem){
  .track-name{ min-height: 4em; }
}

@media (prefers-reduced-motion: reduce){
  .node.is-twin .node-halo{ animation: none; opacity: .35; }
  .dial, .current-flow, .spoke, .node-ping, .link, .rim-spin{ animation: none; }
  .echo-dot{ display: none; }
}

/* ======================================================================
   INQUISITORIAL FILES — inquisitorial-files/

   A book, not an interface. Everything here exists to get out of the way of
   eight and a half thousand words: a warm ground, a serif at a readable
   measure, and pages that turn instead of scrolling.

   Parchment rather than paper or black. The danger with a mid-tone ground is
   muddiness, so every contrast here was measured rather than eyeballed:
   cream on tan 11.1:1, the scribe's italic 7.6:1, gold 9.0:1 — all past the
   7:1 that counts as comfortable for body text. The gold is used only for
   rubrics and never for prose; at reading size it cannot carry a paragraph.
   ====================================================================== */

.files-page{
  --leaf:   #2a2118;   /* the page: dark tan, candle-lit */
  --ink:    #e3d7bd;   /* cream, not white — white on tan glares */
  --ink-2:  #c9bb99;   /* the faded hand: apparatus, folios, the scribe.
                          Lighter than it looks like it should be: the scribe
                          is 64 paragraphs of White Spire, a third of the
                          document, so this is body text and has to clear 7:1
                          against the ground. The first pass sat at 5.31. */
  --accent-rgb: 224 194 75;
  /* --accent must be redeclared alongside the components, not just the
     components. A var() is substituted on the element that declares it, so
     :root's `--accent: rgb(var(--accent-rgb))` already resolved to red there
     and descendants inherit that resolved red however this is retuned. */
  --accent: rgb(var(--accent-rgb));

  /* A book face. System serifs only: nothing is fetched anywhere on this
     site, and these pages get opened straight off disk, where a webfont
     would simply never arrive. */
  --leaf-serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
                Georgia, "Times New Roman", serif;

  background: var(--leaf);
  color: var(--ink);
  font-family: var(--leaf-serif);
  /* It paginates, so it must not also scroll. The two together give you a
     scrollbar that moves nothing, which reads as a broken page. */
  overflow: hidden;
  height: 100%;
}

/* ---------- the volume ---------- */
.volume{
  position: relative;
  height: 100%;
  display: flex;
  flex-direction: column;
  max-width: 46rem;
  margin: 0 auto;
  padding: clamp(1.4rem, 4vh, 3rem) clamp(1.2rem, 5vw, 3.2rem) 0;
}

/* ---------- the leaf ----------
   The log is laid into columns exactly one reading-area wide with the
   overflow hidden, so the browser breaks the text into pages itself and
   re-breaks them on any resize, rotation or text-size change. reader.js only
   ever translates between them — it never measures or slices text. */
/* The leaf on screen scrolls; the ones either side of it are not rendered.
   No columns, no measuring, no transform — the browser does what it is best
   at, which is laying a block of text into a box and scrolling it. */
.reader{
  flex: 1 1 auto;
  overflow-y: auto;
  overflow-x: hidden;
  position: relative;
  min-height: 0;      /* without this the flex child refuses to shrink and the
                         leaf never gets a scrollbar, it just pushes the page */
  scrollbar-width: thin;
  scrollbar-color: rgb(var(--accent-rgb) / .35) transparent;
  overscroll-behavior: contain;   /* stops a flick at the end of a leaf from
                                     dragging the whole page on a phone */
}

.reader::-webkit-scrollbar{ width: 6px; }
.reader::-webkit-scrollbar-track{ background: transparent; }
.reader::-webkit-scrollbar-thumb{
  background: rgb(var(--accent-rgb) / .3);
  border-radius: 3px;
}

.leaf{ display: none; }

/* ---------- turning ----------
   The arriving leaf swings in on a hinge at the edge you turned from: from
   the right going forward, from the left going back. A little perspective on
   the stage makes it a sheet moving rather than a box sliding, and the hinge
   is the outer edge, as a bound page's would be.

   Short. 300ms is long enough to read as a turn and short enough that a
   reader moving quickly through a file is never waiting on it — and the text
   is fully legible for most of the animation rather than at the end of it. */
 /* ---------- the reading stage ----------
   The flex sizing moved here from .reader so the sheet can be a sibling of
   the reading area and cover it exactly, with no measuring. .reader keeps
   the scrolling; this only holds the two of them together.

   NOT ".stage" — that is the album page's class, and it carries
   align-items:center, which stopped the reader stretching and left it
   266px tall inside a 610px box. Third collision of this kind on this
   site after .entry and the ring's positioning; check style.css for a
   name before introducing one. */
.reader-stage{
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  perspective: 1800px;
}

.reader-stage > .reader{ flex: 1 1 auto; min-height: 0; width: 100%; }

/* ---------- the turning sheet ----------
   Laid over the reading area, hinged, and swung away. The leaf underneath
   has already changed by the time it clears, so the swap happens behind a
   page instead of in front of one.

   Paper, a curl of light down the free edge, and the shadow it throws — the
   three things you actually see when a page turns. It carries no text: it is
   edge-on for most of four tenths of a second, and words at that angle read
   as a smear. */
.turn-sheet{
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  display: none;
  transform-style: preserve-3d;
  backface-visibility: hidden;
  background:
    linear-gradient(90deg,
      rgba(0,0,0,.34) 0%, rgba(0,0,0,.10) 14%,
      transparent 42%, rgba(255,226,180,.05) 88%,
      rgba(255,236,200,.12) 100%),
    var(--leaf);
  box-shadow: 14px 0 40px -8px rgba(0,0,0,.75);
}

.turn-sheet.flip-fwd{
  display: block;
  transform-origin: left center;
  animation: sheetFwd .42s cubic-bezier(.42,.02,.32,1) both;
}

.turn-sheet.flip-back{
  display: block;
  /* THE SAME HINGE AS FORWARD. Going back is the forward turn run backwards,
     not its mirror image: the page comes back to the same spine it left
     from. Hinging on the other edge made it a different page returning on
     the other side of the book. It also put the sheet's own shading and its
     shadow the wrong way round, since both are drawn for a left spine — a
     dark gutter at 0%, the lit curl at 100%, and 14px of shadow thrown past
     the free edge.

     THE PATH IS REVERSED; THE EASING IS NOT. Reversing the easing as well is
     the mathematically faithful thing and it feels wrong, because forward
     SETTLES into its finish - .966 of the way there at 80% of the time, .992
     at 90% - and reversed, that settle becomes a dawdle at the start. It
     measured at 8% of the motion done a third of the way in, against
     forward's 28%, and reached halfway at 60% of the duration against
     forward's 40%. A settle is a good way to end a movement and a poor way
     to begin one, so both directions get in and out of the turn the same
     way. */
  transform-origin: left center;
  /* Shorter than forward's .42s, and deliberately so. A back turn cannot
     change the leaf until this sheet is flat, so its whole duration is dead
     time before the page responds, where a forward turn changes immediately
     behind a sheet that is already covering. Equal durations therefore do
     not feel equal - the close reads as the slower of the two. .34s brings
     the wait back into line with how long forward FEELS. */
  animation: sheetBack .34s cubic-bezier(.42,.02,.32,1) both;
}

/* Forward: the page lifts at its free edge and goes over to the left.
   Back: the same page comes back from the left and settles flat, then is
   taken away — literally sheetFwd played in reverse, which is why the
   keyframe stops are the forward ones read bottom to top. */
@keyframes sheetFwd{
  0%   { transform: rotateY(0deg);   opacity: 1; }
  70%  { transform: rotateY(-78deg); opacity: 1; }
  100% { transform: rotateY(-105deg); opacity: 0; }
}

@keyframes sheetBack{
  0%   { transform: rotateY(-105deg); opacity: 0; }
  30%  { transform: rotateY(-78deg);  opacity: 1; }
  100% { transform: rotateY(0deg);    opacity: 1; }
}

.leaf.is-current{
  display: block;
  /* the plain arrival, used on first paint and anywhere no direction is
     given — it must not stay hidden if the turn classes never land */
  animation: leafIn .28s ease both;
}

@keyframes leafIn{
  from { opacity: 0; transform: translateY(.5rem); }
  to   { opacity: 1; transform: none; }
}

 /* Under the sheet these only need to bring the leaf up, not perform a turn
   of their own — the sheet is doing that, and two turns at once reads as a
   wobble. Kept directional and short so the leaf is settled before the
   sheet clears it. */
.leaf.is-current.turn-fwd{
  transform-origin: right center;
  animation: leafTurnFwd .26s cubic-bezier(.22,.7,.3,1) both;
}

.leaf.is-current.turn-back{
  transform-origin: left center;
  animation: leafTurnBack .26s cubic-bezier(.22,.7,.3,1) both;
}

@keyframes leafTurnFwd{
  from { opacity: 0; transform: translateX(5%) rotateY(-7deg); }
  to   { opacity: 1; transform: none; }
}

@keyframes leafTurnBack{
  from { opacity: 0; transform: translateX(-5%) rotateY(7deg); }
  to   { opacity: 1; transform: none; }
}

/* Room under the last line so the text never sits hard against the folio. */
.leaf{ padding-bottom: 1.5rem; }

.log{
  font-size: clamp(1rem, 2.6vw, 1.12rem);
  line-height: 1.72;
  /* Ragged right rather than justified. Justification is the printed-book
     convention and it is what the first pass used, but it only works when
     the engine can hyphenate well and the column is wide enough to absorb
     the slack; in a phone-width column it opens rivers of white space and
     forces awkward breaks. Ragged right is even, quiet and never worse. */
  text-align: left;
}

/* Standard paragraphs: separated by space, no first-line indent, every one
   set the same as the one before it. The earlier version pulled consecutive
   paragraphs together and indented them the way a novel does — correct for a
   printed page, cramped on a screen, and it made a run of dialogue or short
   diary entries look like one dense block. */
.log p{ margin: 0 0 1.15em; text-indent: 0; }

/* ---------- the title page ---------- */
.titlepage{
  padding-top: 12vh;
  text-align: center;
}

.desig{
  margin: 0 0 1.8rem;
  font-size: .74rem;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--accent);
}

.file-title{
  margin: 0 0 2.4rem;
  font-size: clamp(1.5rem, 5vw, 2.3rem);
  font-weight: 400;
  line-height: 1.25;
  color: var(--ink);
}

.status{
  margin: 0;
  font-size: .78rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.status b{ color: var(--accent); font-weight: 400; }

/* ---------- entries ----------
   Every dated entry opens its own leaf, the way a chapter does. A diary is
   written in sittings and a log is filed in sittings, so a page holding two
   of them flattens the pause between — and it is that pause the last entries
   of Mareyn depend on. Short entries therefore leave the rest of the leaf
   empty, which is correct rather than wasteful: it is what a mostly-blank
   page in a diary looks like.

   The first entry is exempt, or it would break away from the title page's
   own leaf, so there is nothing here to force. */

.entry-date{
  margin: 0 0 1.4em;
  font-size: .78rem;
  font-weight: 400;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--accent);
  /* No break-after: avoid here. It was guarding against a date stranded at
     the foot of a page, which cannot happen now that every entry opens its
     own leaf — the date is always the first thing on it. Left in, it fought
     the forced break and produced occasional wholly blank leaves. */
}

/* ---------- the three hands ----------
   White Spire braids a scribe transcribing a journal, the journal itself, and
   one reply from the patron who commissioned it. A printed book separates
   voices by the face, not by furniture, so these are set differently rather
   than boxed: the scribe in italic a size down, the patron in small caps.
   No rules, no insets, nothing that looks like a blockquote widget. */
.log p.v-source{ color: var(--ink); }

.log p.v-scribe{
  font-style: italic;
  font-size: .96em;
  color: var(--ink-2);
}

.log p.v-patron{
  font-variant: small-caps;
  letter-spacing: .04em;
  color: rgb(var(--accent-rgb) / .92);
}

/* ---------- footnote and actions ---------- */
.apparatus{
  padding-top: 1.5rem;
}

.apparatus h2{
  margin: 0 0 1rem;
  font-size: .74rem;
  font-weight: 400;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--accent);
}

.apparatus p{
  margin: 0 0 1em;
  font-size: .95rem;
  line-height: 1.66;
  color: var(--ink-2);
}

.actions p{ color: var(--ink); }

.benediction{
  margin: 2.4em 0 0;
  text-align: center;
  font-style: italic;
  color: rgb(var(--accent-rgb) / .8);
}

/* ---------- turning ----------
   44px is the smallest reliably hittable touch target, and these sit at the
   foot of the page where a thumb already rests rather than in a top corner
   it has to stretch for. */
.folio{
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(1.2rem, 6vw, 2.4rem);
  padding: .9rem 0 .5rem;
}

.turn{
  width: 3rem;
  height: 3rem;
  min-width: 44px;
  min-height: 44px;
  display: grid;
  place-items: center;
  border: 1px solid rgb(var(--accent-rgb) / .3);
  border-radius: 50%;
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  transition: background .2s, border-color .2s, opacity .2s;
  -webkit-tap-highlight-color: transparent;
}

.turn:hover:not(:disabled),
.turn:focus-visible:not(:disabled){
  background: rgb(var(--accent-rgb) / .1);
  border-color: rgb(var(--accent-rgb) / .6);
}

.turn:disabled{ opacity: .22; cursor: default; }

.folio-count{
  font-size: .76rem;
  letter-spacing: .18em;
  color: var(--ink-2);
  min-width: 6ch;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* ---------- leaving ---------- */
.file-nav{
  flex: 0 0 auto;
  display: flex;
  justify-content: space-between;
  gap: 1.2rem;
  padding: .4rem 0 clamp(.8rem, 2vh, 1.4rem);
  font-size: .72rem;
  letter-spacing: .18em;
  text-transform: uppercase;
}

.file-nav a{
  color: var(--ink-2);
  text-decoration: none;
  transition: color .25s;
}
.file-nav a:hover,
.file-nav a:focus-visible{ color: var(--accent); }

/* ---------- the archive ----------
   A contents page, so it scrolls if it must. It is a list rather than a text,
   and paginating a list would be a conceit. */
.archive{
  max-width: 46rem;
  margin: 0 auto;
  padding: clamp(2.5rem, 8vh, 5rem) clamp(1.2rem, 5vw, 3.2rem) 4rem;
  height: 100%;
  overflow-y: auto;
}

.archive-head{ margin-bottom: 2.6rem; }

.sigil-title{
  margin: 0;
  font-size: clamp(1.3rem, 4vw, 1.9rem);
  font-weight: 400;
  letter-spacing: .12em;
  color: var(--accent);
}

.archive-list{ list-style: none; margin: 0; padding: 0; }

.archive-list li{ border-bottom: 1px solid rgb(var(--accent-rgb) / .16); }
.archive-list li:first-child{ border-top: 1px solid rgb(var(--accent-rgb) / .16); }

.record{
  display: grid;
  grid-template-columns: 1fr auto;
  gap: .25rem 1.2rem;
  padding: 1.15rem .2rem;
  text-decoration: none;
  color: var(--ink);
  transition: background .25s, padding-left .25s;
}

.record:hover,
.record:focus-visible{
  background: rgb(var(--accent-rgb) / .06);
  padding-left: .7rem;
}

.r-desig{
  grid-column: 1;
  font-size: .72rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--accent);
}

.r-status{
  grid-column: 2;
  grid-row: 1;
  text-align: right;
  font-size: .72rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.r-title{
  grid-column: 1 / -1;
  font-size: 1.1rem;
  color: var(--ink);
}

.archive-note{
  margin: 2.2rem 0 0;
  font-size: .8rem;
  font-style: italic;
  color: var(--ink-2);
  opacity: .75;
}

@media (prefers-reduced-motion: reduce){
  .leaf.is-current{ animation: none; }
  .record, .turn{ transition: none; }
}

/* ---------- the grain ----------
   The difference between a tan background and a leaf. Static, not drifting:
   the landing page animates its grain because that scene is alive, but a page
   of text that shimmers is unreadable. One fixed layer, painted once, so it
   costs nothing per frame. */
body.files-page::before{
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: .05;
  mix-blend-mode: multiply;   /* darkens into the parchment rather than
                                 laying grey haze on top of it */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* and a vignette, so the leaf has edges rather than running off the screen */
body.files-page::after{
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(120% 100% at 50% 45%, transparent 55%, rgba(20,14,8,.42) 100%);
}

.volume, .archive{ position: relative; z-index: 1; }

/* ---------- the running head ----------
   Constant across every leaf, as in a printed book: it sits outside the
   paginated flow so it does not turn with the text. Faint, because it is for
   glancing at rather than reading. */
.running-head{
  flex: 0 0 auto;
  margin: 0 0 1.4rem;
  font-size: .66rem;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--ink-2);
  opacity: .6;
  text-align: center;
  border-bottom: 1px solid rgb(var(--accent-rgb) / .14);
  padding-bottom: .8rem;
}

/* ---------- the drop cap ----------
   On the first prose paragraph of each entry, which now always sits at the
   top of a fresh leaf — which is the only place a drop cap belongs. Never on
   a salutation: "Diary Dearest," is furniture, not an opening line. */
.log p.opens::first-letter{
  float: left;
  font-size: 3.1em;
  line-height: .82;
  padding: .06em .1em 0 0;
  color: var(--accent);
}

/* An italic drop cap on the scribe's letters would collide with the line
   below it, so his open a size smaller and upright. */
.log p.v-scribe.opens::first-letter{
  font-size: 2.5em;
  font-style: normal;
}

/* ---------- letter furniture ----------
   A salutation and a signature are not sentences and should not be set as
   them. Both sit apart from the prose; the signature is set to the right and
   in italic, the way a hand-signed name does.

   This is also what carries "Hun-" — the diary's last line, a signature that
   stops mid-name. It gets the same treatment as every "Hunlen" before it,
   which is what makes the break visible. */
.log p.r-salute{
  margin: 0 0 1.1em;
  font-style: italic;
  color: var(--ink);
}

.log p.r-signoff{
  margin: 1.4em 0 0;
  text-align: right;
  font-style: italic;
  color: var(--ink-2);
  letter-spacing: .04em;
}

/* two signature lines in a row — "Your Humble Scribe," then the name — should
   read as one block rather than as two separated things */
.log p.r-signoff + p.r-signoff{ margin-top: .2em; }

/* ---------- the apparatus as a pasted slip ----------
   The Footnote and the Actions are the Inquisition writing on somebody else's
   document. Given the same ground as the prose they read as two more chapters;
   given their own they read as an insertion, which is what they are — and the
   shift from a dead child's diary to a clerk ordering a town liquidated lands
   as the intrusion it should be.

   A ground and a rule rather than a floating box: this content is inside the
   paginated flow, so a bordered card would be sliced in half at every page
   break. A background and a left rule fragment cleanly. */
.apparatus{
  background: rgba(20,14,8,.30);
  border-left: 2px solid rgb(var(--accent-rgb) / .45);
  padding: 1.4rem 1.5rem 1.2rem;
  margin-top: 0;
}

.apparatus h2{
  display: flex;
  align-items: center;
  gap: .8rem;
}

/* a clerk's mark against the heading */
.apparatus h2::before{
  content: "";
  width: .5rem;
  height: .5rem;
  flex: 0 0 auto;
  background: var(--accent);
  transform: rotate(45deg);
  opacity: .8;
}

/* ---------- the masthead ----------
   Set, not drawn. sigilskript is the script of the things the order
   catalogues, not of the order — so the archive names itself in its own
   plain hand, and the glyphs are saved for the objects. */
.archive-title{
  margin: 0;
  font-size: clamp(1.4rem, 5vw, 2.2rem);
  font-weight: 400;
  letter-spacing: .26em;
  text-indent: .26em;      /* offsets the trailing letter-space so it centres */
  text-transform: uppercase;
  color: var(--accent);
  line-height: 1.3;
}

/* the plain words, kept for anything that cannot draw the glyphs */
.sr{
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- status as treatment ----------
   The word is already there; this makes the row read as its status without
   introducing a second signal colour. Active is fully inked, Dormant is faded
   back, Contained is ruled through — sealed rather than deleted, so the rule
   goes across the row instead of through the letters. */
.record.status-active .r-title{ color: var(--ink); }

.record.status-dormant{ opacity: .6; }

.record.status-contained .r-title{
  position: relative;
  color: var(--ink-2);
}

.record.status-contained .r-title::after{
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: rgb(var(--accent-rgb) / .5);
}

/* Active files are the ones still moving, so they get the only mark on the
   list — a small open square against the designation. */
.record.status-active .r-desig::after{
  content: " \25A1";
  opacity: .85;
}

/* ======================================================================
   THE WORKING — glow and ornament

   Split deliberately: the archive is allowed to be theatrical, the reading
   leaves are not. Eight thousand words is a long sit, and anything that
   pulses next to a paragraph is something the eye keeps returning to
   instead of reading. So the index breathes and turns, and the logs get
   warmth without motion.

   All of it is text-shadow and one transform. No drop-shadow filters on
   anything that animates — that rule is in Performance for good reason,
   and it is the same trap the album page fell into.
   ====================================================================== */

/* ---------- status as a living thing ----------
   The glow says what the word says. Active is still moving, so it breathes.
   Contained is sealed under holy silver — lit, but held at one intensity and
   never varying. Dormant is dark, and gets nothing: the absence is the tell.
   One colour throughout, so the palette rule holds. */

.record.status-active .r-title,
.status-active b{
  color: var(--accent);
  text-shadow: 0 0 8px rgb(var(--accent-rgb) / .55),
               0 0 22px rgb(var(--accent-rgb) / .25);
  animation: wardBreathe 7s ease-in-out infinite;
}

@keyframes wardBreathe{
  0%, 100% { opacity: .82; }
  50%      { opacity: 1; }
}

/* sealed: lit but fixed, and the rule across it reads as the seal */
.record.status-contained .r-title,
.status-contained b{
  text-shadow: 0 0 7px rgb(var(--accent-rgb) / .3);
}

.record.status-contained .r-title::after{
  box-shadow: 0 0 6px rgb(var(--accent-rgb) / .45);
}

/* dormant keeps its silence — no shadow, no motion */

/* ---------- the masthead ----------
   The same slow breath the landing page's sigil carries, so arriving here
   from the vista feels like the same hand made both. */
.archive-title{
  text-shadow: 0 0 12px rgb(var(--accent-rgb) / .45),
               0 0 34px rgb(var(--accent-rgb) / .2);
  animation: wardBreathe 9s ease-in-out infinite;
}


/* ---------- the archive, dressed ---------- */
.archive-head{ text-align: center; }

/* the rules between records glow faintly, so the list reads as inscribed
   rather than as a table */
.archive-list li{
  border-bottom-color: rgb(var(--accent-rgb) / .22);
  box-shadow: 0 1px 6px -3px rgb(var(--accent-rgb) / .5);
}

.record{ position: relative; }

/* a lozenge in the margin, lit only for the record under the pointer — the
   mark travels down the list with you */
.record::before{
  content: "";
  position: absolute;
  left: -.9rem;
  top: 50%;
  width: .38rem;
  height: .38rem;
  margin-top: -.19rem;
  background: var(--accent);
  transform: rotate(45deg) scale(0);
  transition: transform .25s ease, box-shadow .25s ease;
  box-shadow: 0 0 8px rgb(var(--accent-rgb) / .8);
}

.record:hover::before,
.record:focus-visible::before{ transform: rotate(45deg) scale(1); }

.record:hover,
.record:focus-visible{
  background: linear-gradient(90deg,
              rgb(var(--accent-rgb) / .09), rgb(var(--accent-rgb) / .01));
}

.record:hover .r-title,
.record:focus-visible .r-title{
  text-shadow: 0 0 10px rgb(var(--accent-rgb) / .5);
}

/* the closing line set as an inscription rather than a caption */
.archive-note{
  text-align: center;
  letter-spacing: .1em;
  text-shadow: 0 0 12px rgb(var(--accent-rgb) / .2);
}

.archive-note::before,
.archive-note::after{
  content: "";
  display: block;
  width: 3.5rem;
  height: 1px;
  margin: 1.2rem auto;
  background: linear-gradient(90deg, transparent,
              rgb(var(--accent-rgb) / .5), transparent);
}
.archive-note::after{ margin-bottom: 0; }

/* ---------- warmth on the logs, without motion ----------
   The rubrics and the initial are lit; the prose is not. Nothing here moves. */
.entry-date{ text-shadow: 0 0 10px rgb(var(--accent-rgb) / .35); }

.log p.opens::first-letter{
  text-shadow: 0 0 14px rgb(var(--accent-rgb) / .45),
               0 0 34px rgb(var(--accent-rgb) / .2);
}

.desig{ text-shadow: 0 0 10px rgb(var(--accent-rgb) / .35); }

.file-title{
  text-shadow: 0 0 18px rgb(var(--accent-rgb) / .16);
}

.apparatus h2::before{ box-shadow: 0 0 7px rgb(var(--accent-rgb) / .7); }

.benediction{ text-shadow: 0 0 12px rgb(var(--accent-rgb) / .3); }

/* the turn buttons pick up a little light as you reach for them */
.turn:hover:not(:disabled),
.turn:focus-visible:not(:disabled){
  box-shadow: 0 0 14px rgb(var(--accent-rgb) / .3);
  color: var(--accent);
}

/* only the way back, so it centres rather than sitting in a corner */
.file-nav-single{ justify-content: center; }

@media (prefers-reduced-motion: reduce){
  .archive-title,
  .record.status-active .r-title,
  .status-active b{ animation: none; }
  .record::before{ transition: none; }
}

/* ======================================================================
   THE ARCHIVE — a ledger seen through a window

   The register is lit parchment inside a silver-inlaid frame; everything
   around it is void, with filaments drifting off the edge (archive.js).

   Silver is a second material, not a second signal. The palette rule on
   this site is one signal colour at a time, and gold keeps that job —
   status, rubrics, anything that means something. Silver is what the frame
   and the rules are *made of*, which is a different claim entirely, and it
   is the holy silver the order seals things with.
   ====================================================================== */

.files-page{
  --silver:      #c9cdd6;
  --silver-dim:  #7d8593;
  --void:        #0a0908;
}

/* The archive page alone is void-grounded; the log leaves stay parchment
   edge to edge, because a reader should not be looking through a letterbox
   at eight thousand words.

   A class rather than :has(.void-field), which would have been neater and is
   a bet on Firefox 121+. This site has already lost an evening to assuming a
   feature behaved the same in Gecko as in Blink; a body class cannot fail. */
body.archive-page{ background: var(--void); }

.void-field{
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
  pointer-events: none;
}

.archive{
  position: relative;
  z-index: 1;
  display: grid;
  /* stretch, not centre: the window is meant to fill the height. place-items
     centre would size it to its content and leave it floating in the middle. */
  align-items: stretch;
  justify-items: center;
  max-width: none;
  height: 100%;
  padding: clamp(1rem, 4vh, 2.5rem) clamp(.9rem, 4vw, 2rem);
  overflow: hidden;         /* the window scrolls inside itself, not the page */
}

/* ---------- the window ---------- */
.window{
  position: relative;
  width: min(46rem, 100%);
  height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--leaf);
  padding: clamp(1.6rem, 4vw, 2.8rem) clamp(1.2rem, 4vw, 2.6rem);
  /* the inlay: a silver hairline, a hair of dark between, and the lit edge
     of the parchment inside it */
  border: 1px solid rgb(201 205 214 / .38);
  box-shadow:
    0 0 0 1px rgb(10 9 8 / .9),
    0 0 0 2px rgb(201 205 214 / .12),
    0 0 40px -8px rgb(201 205 214 / .18),
    0 30px 80px -30px #000;
}

/* corner marks, where an inlay would be pinned */
.window::before,
.window::after{
  content: "";
  position: absolute;
  width: 10px;
  height: 10px;
  border: 1px solid rgb(201 205 214 / .5);
  pointer-events: none;
}
.window::before{ top: 5px; left: 5px; border-right: 0; border-bottom: 0; }
.window::after{ bottom: 5px; right: 5px; border-left: 0; border-top: 0; }

/* ---------- head ---------- */
.archive-head{
  text-align: center;
  margin-bottom: 1.6rem;
  padding-bottom: 1.2rem;
  border-bottom: 1px solid rgb(201 205 214 / .2);
}

.archive-sub{
  margin: .7rem 0 0;
  font-size: .68rem;
  letter-spacing: .34em;
  text-indent: .34em;
  text-transform: uppercase;
  color: var(--silver-dim);
}

/* ---------- the ledger ----------
   Three ruled columns, as a bound register has: what it is called, what it
   is, and what was done about it. */
.ledger{
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgb(201 205 214 / .3) transparent;
}

.ledger-head,
.record{
  display: grid;
  grid-template-columns: 5.5rem 1fr 6.5rem;
  gap: 0 1.1rem;
  align-items: baseline;
}

.ledger-head{
  padding: 0 .3rem .55rem;
  font-size: .58rem;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--silver-dim);
  border-bottom: 1px solid rgb(201 205 214 / .32);
}

.ledger-head span:last-child{ text-align: right; }

.archive-list{ list-style: none; margin: 0; padding: 0; }

/* hairline between records — inlay, not a table border */
.archive-list li{
  border-top: 0;
  border-bottom: 1px solid rgb(201 205 214 / .12);
  box-shadow: none;
}
.archive-list li:first-child{ border-top: 0; }

.record{
  position: relative;
  padding: .95rem .3rem;
  text-decoration: none;
  color: var(--ink);
  transition: background .25s ease;
}

.r-desig{
  grid-column: 1;
  font-size: .74rem;
  letter-spacing: .14em;
  color: var(--silver);
  font-variant-numeric: tabular-nums;
  text-transform: none;
}

.r-title{
  grid-column: 2;
  font-size: 1.02rem;
  color: var(--ink);
  line-height: 1.35;
}

.r-status{
  grid-column: 3;
  text-align: right;
  font-size: .62rem;
  letter-spacing: .2em;
  text-transform: uppercase;
}

/* the marginal lozenge sits in the frame's gutter now */
.record::before{ left: -.55rem; }

.record:hover,
.record:focus-visible{
  background: linear-gradient(90deg,
              rgb(201 205 214 / .07), rgb(201 205 214 / .01));
  padding-left: .3rem;      /* the row must not shift; only the wash moves */
}

/* ---------- foot ---------- */
.archive-note{
  flex: 0 0 auto;
  margin: 1.4rem 0 0;
  padding-top: 1.1rem;
  border-top: 1px solid rgb(201 205 214 / .2);
  font-size: .76rem;
}
.archive-note::before{ display: none; }
.archive-note::after{ display: none; }

.window .file-nav{ margin-top: 1rem; padding-bottom: 0; border-top: 0; }

@media (max-width: 34rem){
  /* the designation moves above the title rather than squeezing three
     columns into a phone, and the disposition holds the right margin */
  .ledger-head{ display: none; }
  .record{ grid-template-columns: 1fr auto; gap: .2rem .8rem; }
  .r-desig{ grid-column: 1; grid-row: 1; }
  .r-status{ grid-column: 2; grid-row: 1; }
  .r-title{ grid-column: 1 / -1; grid-row: 2; }
}

/* ======================================================================
   THE LOGS — lit, not animated

   The reading pages get the faintest gutter and nothing else. Eight and a
   half thousand words is a long sit, and anything that moves beside a
   paragraph is something the eye keeps going back to instead of reading.
   This is opacity on one fixed layer, which stays on the compositor, and it
   is slow and shallow enough to be felt rather than seen.
   ====================================================================== */
body.log-page::after{
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(120% 80% at 50% 108%,
              rgba(224,194,75,.07), transparent 62%);
  animation: leafGutter 11s ease-in-out infinite;
}

/* Odd stops rather than an even pulse, so it does not read as a heartbeat. */
@keyframes leafGutter{
  0%   { opacity: .82; }
  17%  { opacity: 1; }
  31%  { opacity: .88; }
  49%  { opacity: .97; }
  63%  { opacity: .8; }
  81%  { opacity: .95; }
  100% { opacity: .82; }
}

@media (prefers-reduced-motion: reduce){
  body.log-page::after{ animation: none; opacity: .9; }
}

/* ======================================================================
   THE ENGINE — a magic computer

   Their Sacred Instruments are modern machines held as holy objects, so
   this is a readout that is also an altar. Not a terminal with occult
   decals on it, and not a rune circle pretending to be software — the two
   have to be the same object.

   What makes it read as arcane rather than as sci-fi:

     the light is IN the surface   channels cut into a dark face and lit
                                   from inside, not panels sitting on top
     the geometry is drawn         circles, brackets, tick marks — an
                                   astrolabe's vocabulary. No rounded
                                   rectangles, no icons, nothing that
                                   looks designed by a UI team
     the type is a machine hand    the site's mono, spaced like an
                                   inscription rather than set like a label
     it is doing something         a hand sweeps the face. An instrument
                                   that is idle is a picture of one

   The archive is the instrument; the files are the documents it holds.
   That is why this is a lit dark face and the reading leaves are paper —
   you work the machine to pull a record, and the record is a book.

   --charge is published by archive.js, 0.9..1.1. Falls back to 1 wherever
   the script has not run.
   ====================================================================== */

html.files-page{ --charge: 1; }

/* the dark the instrument stands in */
body.archive-page{ background: #07070a; }

body.archive-page::after{
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(70% 60% at 50% 50%,
      rgba(224,194,75,calc(.055 * var(--charge))), transparent 70%),
    radial-gradient(130% 110% at 50% 45%, transparent 35%, rgba(2,2,4,.85) 100%);
}

.archive{
  position: relative;
  z-index: 1;
  display: grid;
  align-items: stretch;
  justify-items: center;
  max-width: none;
  height: 100%;
  padding: clamp(.8rem, 3vh, 2rem) clamp(.8rem, 3vw, 2rem);
  overflow: hidden;
}

/* ---------- the face ---------- */
.engine{
  position: relative;
  width: min(52rem, 100%);
  height: 100%;
  display: flex;
  flex-direction: column;
  padding: clamp(1.6rem, 4vw, 2.8rem) clamp(1.2rem, 4vw, 2.6rem) clamp(1rem, 3vw, 1.8rem);
  overflow: hidden;

  /* Dark stone with the charge behind it, brightest at the centre where the
     instrument is working. The light comes from within the face. */
  background:
    radial-gradient(120% 80% at 50% 42%,
      rgba(224,194,75,calc(.055 * var(--charge))), transparent 62%),
    linear-gradient(#0e0e13, #0a0a0e);

  /* the cut edge: a hairline of the channel light around the whole face */
  border: 1px solid rgb(var(--accent-rgb) / .28);
  box-shadow:
    inset 0 0 60px -20px rgb(var(--accent-rgb) / calc(.5 * var(--charge))),
    inset 0 0 0 1px rgba(0,0,0,.85),
    0 0 40px -12px rgb(var(--accent-rgb) / calc(.28 * var(--charge))),
    0 24px 70px -30px #000;
}

/* engraved corner geometry, cut into the face */
.etch{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
  color: rgb(var(--accent-rgb) / .5);
  stroke-width: 1;
}

/* the phosphor grain of the face itself */
.engine::before{
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: .5;
  background: repeating-linear-gradient(
    to bottom,
    rgb(var(--accent-rgb) / .04) 0px,
    rgb(var(--accent-rgb) / .04) 1px,
    transparent 1px,
    transparent 3px);
}

/* ---------- the hand ----------
   One slow pass down the face. Transform only, so it rides the compositor
   and costs nothing — the album page's rule about what may animate. */
.sweep{
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 34%;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom,
    transparent 0%,
    rgb(var(--accent-rgb) / .035) 62%,
    rgb(var(--accent-rgb) / .10) 92%,
    rgb(var(--accent-rgb) / .5) 99.4%,
    transparent 100%);
  animation: sweepDown 7.5s linear infinite;
  will-change: transform;
}

@keyframes sweepDown{
  from { transform: translateY(-40%); }
  to   { transform: translateY(300%); }
}

/* Superseded further down by the version that also excludes .ring-etch and
   .dead-corner. Left as a comment rather than a live rule: at four classes
   deep it out-specifies .ring-etch's single class and silently forced the
   ring to position:relative, collapsing its height and shrinking the circle
   to a third of the face. */

.motes{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 1;
  pointer-events: none;
}

/* ---------- the machine's hand ----------
   Mono, spaced like an inscription rather than set like a label. This is
   the instrument speaking, so it never uses the book face. */
.archive-head{
  text-align: left;
  margin-bottom: 1.3rem;
  padding-bottom: 1rem;
  padding-right: 0;
  border-bottom: 1px solid rgb(var(--accent-rgb) / .26);
}

.archive-title{
  font-family: var(--mono);
  color: var(--accent);
  font-size: clamp(1rem, 3.2vw, 1.35rem);
  letter-spacing: .34em;
  text-indent: .34em;
  text-shadow: 0 0 10px rgb(var(--accent-rgb) / calc(.7 * var(--charge))),
               0 0 28px rgb(var(--accent-rgb) / calc(.35 * var(--charge)));
  animation: none;
}

.archive-sub{
  font-family: var(--mono);
  color: rgb(var(--accent-rgb) / .5);
  text-align: left;
}

.ledger-head{
  font-family: var(--mono);
  color: rgb(var(--accent-rgb) / .45);
  border-bottom-color: rgb(var(--accent-rgb) / .3);
}

.archive-list li{ border-bottom-color: rgb(var(--accent-rgb) / .14); }

/* a lamp, the designation, the name, the disposition */
.ledger-head,
.record{ grid-template-columns: .8rem 5.5rem 1fr 6.5rem; }
/* no padding-left here: the head carries an empty leading cell for the
   rune column instead, so it aligns with the rows by structure. */

.r-rune{
  grid-column: 1;
  align-self: center;
  width: .5rem;
  height: .5rem;
  transform: rotate(45deg);
  border: 1px solid rgb(var(--accent-rgb) / .55);
}

.r-desig{
  grid-column: 2;
  font-family: var(--mono);
  color: rgb(var(--accent-rgb) / .85);
  text-shadow: 0 0 8px rgb(var(--accent-rgb) / .35);
}

.r-title{
  grid-column: 3;
  font-family: var(--mono);
  font-size: .92rem;
  letter-spacing: .06em;
  color: #cfcbc0;
}

.r-status{
  grid-column: 4;
  font-family: var(--mono);
  color: rgb(var(--accent-rgb) / .55);
}

.record{ padding: .9rem .3rem; }
.record::before{ content: none; }

.record:hover,
.record:focus-visible{
  background: linear-gradient(90deg, rgb(var(--accent-rgb) / .09), transparent 72%);
}
.record:hover .r-title,
.record:focus-visible .r-title{
  color: #fff;
  text-shadow: 0 0 10px rgb(var(--accent-rgb) / .6);
}

/* ---------- status as a lit lamp ----------
   The instrument reports by what is lit. Active burns and breathes,
   Contained is held at one level, Dormant is dark and the row goes with it. */
.record.status-active .r-rune{
  background: var(--accent);
  box-shadow: 0 0 10px rgb(var(--accent-rgb) / .9),
              0 0 22px rgb(var(--accent-rgb) / .45);
  animation: runeBreathe 3.4s ease-in-out infinite;
}
.record.status-active .r-title{ color: #efe9d8; text-shadow: none; animation: none; }
.record.status-active .r-status{ color: var(--accent); }

@keyframes runeBreathe{
  0%, 100% { opacity: .62; }
  50%      { opacity: 1; }
}

.record.status-contained .r-rune{
  background: rgb(var(--accent-rgb) / .35);
  box-shadow: 0 0 7px rgb(var(--accent-rgb) / .3);
}
.record.status-contained .r-title{ color: #b6b1a6; text-shadow: none; }
.record.status-contained .r-title::after{ background: rgb(var(--accent-rgb) / .45); }

.record.status-dormant{ opacity: .5; }
.record.status-dormant .r-rune{ background: none; border-color: rgb(var(--accent-rgb) / .3); }

/* ---------- foot ---------- */
.archive-note{
  font-family: var(--mono);
  font-size: .68rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  font-style: normal;
  text-align: left;
  color: rgb(var(--accent-rgb) / .42);
  border-top-color: rgb(var(--accent-rgb) / .26);
  text-shadow: none;
}
.archive-note::before, .archive-note::after{ display: none; }

.engine .file-nav{
  margin-top: .8rem;
  padding-bottom: 0;
  border-top: 0;
  justify-content: flex-start;
}
.engine .file-nav a{ font-family: var(--mono); color: rgb(var(--accent-rgb) / .5); }
.engine .file-nav a:hover,
.engine .file-nav a:focus-visible{ color: var(--accent); }

@media (max-width: 34rem){
  .record{ grid-template-columns: .8rem 1fr auto; }
  .r-rune{ grid-column: 1; grid-row: 1; }
  .r-desig{ grid-column: 2; grid-row: 1; }
  .r-status{ grid-column: 3; grid-row: 1; }
  .r-title{ grid-column: 2 / -1; grid-row: 2; }
}

@media (prefers-reduced-motion: reduce){
  html.files-page{ --charge: 1; }
  .sweep{ animation: none; opacity: .35; }
  .record.status-active .r-rune{ animation: none; opacity: 1; }
}

/* ======================================================================
   ARCANE OVER MACHINE

   In this world the two are the same thing, which is exactly why the page
   has to lean. Left alone, a lit rectangle with mono type on it reads as a
   terminal every time. Three things tip it:

     circles over rectangles   a grid of ruled boxes is a panel. A ring
                               with an inscription round it is a circle of
                               power — HANDOFF says the same about the
                               album's rim, that it is what separates a
                               magic circle from an instrument panel
     wear over precision       machines are regular. A consecrated object
                               is hand-cut, scarred, and maintained by
                               people who do not understand it
     pooled light over even    a face lit evenly is backlit glass. A face
                               lit in patches, with one corner where the
                               glow no longer reaches, is powered
   ====================================================================== */

/* ---------- the ring ----------
   Behind everything, at the size the face can actually hold. It fits rather
   than fills: a stretched circle is an oval, and an oval is an accident. */
.ring-etch{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
  color: rgb(var(--accent-rgb) / .34);
  stroke-width: 1;
  opacity: calc(.6 + .25 * var(--charge));
}

.rim-text{
  font-family: var(--mono);
  /* 10.3px, not 9.5. At 9.5 the band's natural width was 903.5 against a
     980.18 circumference, so textLength had to stretch it by 76.7 units and
     every gap carried 0.64 of padding it did not ask for. Sized so the text
     very nearly fits on its own, the adjustment falls to almost nothing and
     the spacing is the font's own. textLength stays as the guarantee that it
     closes exactly, not as the thing doing the work.

     No letter-spacing here: it would fight textLength, with both trying to
     set the same thing. */
  font-size: 10.3px;
  text-transform: uppercase;
  fill: rgb(var(--accent-rgb) / .62);
  stroke: none;
}

/* the ring turns, slowly enough that you are never sure it is moving —
   transform only, on one element, so it stays on the compositor */
.ring-etch{ animation: ringTurn 240s linear infinite; }
@keyframes ringTurn{ to { transform: rotate(360deg); } }

/* ---------- the wear ----------
   Pitting across the stone, and the glow broken up so it pools instead of
   washing evenly. Multiply so it eats into the face rather than laying haze
   over it. */
.engine::after{
  content: "";
  position: absolute;
  inset: 0;
  /* UNDER the engraving, not over it. At the same z-index this paints after
     the ring (::after comes later in paint order) and the pitting chewed
     visible holes in the inscription — which reads as the band failing to
     line up rather than as wear on the stone. The engraving is cut INTO the
     face, so the wear belongs beneath it. */
  z-index: 0;
  pointer-events: none;
  opacity: .42;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='p'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.035 .05' numOctaves='5'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.1 .78'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23p)'/%3E%3C/svg%3E");
  background-size: 340px 340px;
}

/* ---------- pooled light ----------
   Not one even glow. Three offsets at different strengths, so the charge
   gathers in places and thins between them. */
.engine{
  background:
    radial-gradient(46% 34% at 34% 30%,
      rgb(var(--accent-rgb) / calc(.075 * var(--charge))), transparent 70%),
    radial-gradient(38% 30% at 71% 58%,
      rgb(var(--accent-rgb) / calc(.05 * var(--charge))), transparent 72%),
    radial-gradient(60% 46% at 50% 88%,
      rgb(var(--accent-rgb) / calc(.03 * var(--charge))), transparent 76%),
    linear-gradient(#0e0e13, #0a0a0e);
}

/* the corner the light has left */
.dead-corner{
  position: absolute;
  right: 0;
  bottom: 0;
  width: 38%;
  height: 28%;
  z-index: 0;              /* beneath the engraving, as the pitting is */
  pointer-events: none;
  background: radial-gradient(120% 120% at 100% 100%,
              rgba(2,2,4,.7), transparent 66%);
}

/* ---------- the hand, softened ----------
   Slower and dimmer than a scanning bar. A blade of light travelling at
   radar speed is a machine; light moving unhurriedly across a surface is
   something else looking at you. */
.sweep{
  height: 46%;
  animation-duration: 15s;
  background: linear-gradient(to bottom,
    transparent 0%,
    rgb(var(--accent-rgb) / .02) 60%,
    rgb(var(--accent-rgb) / .06) 90%,
    rgb(var(--accent-rgb) / .22) 99%,
    transparent 100%);
}

/* the content sits above every layer of the face */
.engine > *:not(.motes):not(.sweep):not(.etch):not(.ring-etch):not(.dead-corner){
  position: relative;
  z-index: 2;
}

/* ---------- the register, unpicked from the grid ----------
   Hairlines rather than table rules, and the heads set as an inscription so
   the list reads as something scribed onto the face. */
.ledger-head{
  border-bottom-color: rgb(var(--accent-rgb) / .22);
  letter-spacing: .3em;
  font-size: .55rem;
}

.archive-list li{ border-bottom-color: rgb(var(--accent-rgb) / .1); }

/* the rune becomes a cut mark rather than a status LED */
.r-rune{
  width: .45rem;
  height: .45rem;
  border-radius: 50%;
  border: 1px solid rgb(var(--accent-rgb) / .5);
  transform: none;
}

.record.status-active .r-rune{
  border-color: rgb(var(--accent-rgb) / .9);
}

.record.status-dormant .r-rune{
  border-style: dashed;
  border-color: rgb(var(--accent-rgb) / .35);
}

@media (max-width: 34rem){
  /* the ring would be a coin at this width, and a coin reads as a logo */
  .ring-etch{ opacity: calc(.3 + .2 * var(--charge)); }
  .rim-text{ font-size: 12px; }
}

@media (prefers-reduced-motion: reduce){
  .ring-etch{ animation: none; }
}

/* ======================================================================
   THE GLASS — paper seen through the instrument

   The archive is the machine's face; the logs are what the machine is
   showing you. So the same phosphor, the same bezel and the same cut
   corners lie over the reading leaves, and the parchment sits behind them
   rather than being replaced by them.

   Everything here is held far below what it would be on the index, for one
   reason: eight and a half thousand words. Glass that you notice while
   reading is glass that has failed. The scanline is .028 against the face's
   .04, nothing on this layer moves at all, and the paper keeps its own
   colour and its own grain underneath.
   ====================================================================== */

.glass{
  position: fixed;
  inset: 0;
  z-index: 5;              /* over the text — that is the whole point */
  pointer-events: none;    /* and never in the way of it */

  /* the phosphor, held below the face's — see the note above */
  background-image:
    /* the sheen across the pane, off-axis so it is not a stripe */
    linear-gradient(112deg,
      transparent 26%,
      rgba(255,246,214,.022) 42%,
      rgba(255,246,214,.008) 52%,
      transparent 68%),
    /* the scanline itself */
    repeating-linear-gradient(
      to bottom,
      rgb(var(--accent-rgb) / .028) 0px,
      rgb(var(--accent-rgb) / .028) 1px,
      transparent 1px,
      transparent 3px);

  /* the bezel: the pane is set into something, so its edges eat the light */
  box-shadow:
    inset 0 0 90px rgba(8,6,4,.5),
    inset 0 0 220px rgba(8,6,4,.35);
}

/* the curve of the pane, darkening into the corners */
.glass::after{
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(128% 108% at 50% 46%,
              transparent 58%, rgba(8,6,4,.5) 100%);
}

/* ---------- the screen's cut corners ----------
   The same marks the engine's face carries, so the two pages are plainly the
   same object. Fainter here: on the index they frame the instrument, on a
   page of prose they only need to be noticed once.

   Pinned to the ENGINE'S box, not the volume's, so the frame does not jump
   when you open a record. The index's face is `min(52rem, 100%)` centred
   inside .archive's padding, which resolves to exactly

       max(pad, 50% - 26rem)

   on each side — 26rem being half of 52. Matching that here puts the two
   frames on the same rectangle at every width, without restructuring the
   reading page around the index's layout. The text keeps its own narrower
   measure inside the frame. */
.log-page .etch{
  position: fixed;
  top:    clamp(.8rem, 3vh, 2rem);
  bottom: clamp(.8rem, 3vh, 2rem);
  left:   max(clamp(.8rem, 3vw, 2rem), calc(50% - 26rem));
  top:    clamp(.8rem, 3vh, 2rem);
  /* BOTH dimensions stated, never auto. An SVG is a replaced element, so an
     `auto` dimension is taken from the viewBox's aspect ratio and the
     opposite offset is dropped as over-constrained. Leaving height auto made
     the frame 832 square and hung its foot 80px low; fixing height alone
     then made it 752 square and pulled its right edge 80px in. Stating both
     is the only arrangement that holds.

     These are the engine's own box: `min(52rem, 100%)` centred inside
     .archive's padding resolves to max(pad, 50% - 26rem) on each side, 26rem
     being half of 52. dvh second so a phone's collapsing URL bar does not
     leave the frame short. */
  right: auto;
  bottom: auto;
  width:  min(52rem, calc(100vw - 2 * clamp(.8rem, 3vw, 2rem)));
  height: calc(100vh - 2 * clamp(.8rem, 3vh, 2rem));
  height: calc(100dvh - 2 * clamp(.8rem, 3vh, 2rem));
  z-index: 6;              /* over the glass, as the engine's are over its face */
  pointer-events: none;
  color: rgb(var(--accent-rgb) / .34);
  stroke-width: 1;
}

/* the volume's content stays above its own frame */
.log-page .volume > *:not(.etch){ position: relative; z-index: 1; }

/* ---------- the charge, carried over ----------
   The leaves already gutter faintly. Retimed to the engine's own tremor so
   the two pages breathe alike rather than each keeping its own clock. */
body.log-page::after{
  background: radial-gradient(120% 80% at 50% 108%,
              rgb(var(--accent-rgb) / .075), transparent 62%);
}

@media (prefers-reduced-motion: reduce){
  .glass{ box-shadow: inset 0 0 90px rgba(8,6,4,.5); }
}

/* ======================================================================
   THE WARDING — one pass, on load

   The order does not simply read these. A record that cost them Sacred
   Instruments of Sight to inscribe is read from behind wards, and this is
   that: the marks are struck, they hold while the light comes up behind
   them, and they go once the leaf is safe to look at.

   Rules it is built to:

     it runs once      nothing here loops and nothing survives the first
                       two seconds. A ward you can still see is a ward
                       that did not take
     reading first     the text is legible by 0.9s. The wards go on fading
                       after that, so the page is never held hostage to
                       its own opening
     it cannot strand  every animation is fill-mode both and ends at the
                       resting state, so a dropped frame or a backgrounded
                       tab leaves the page readable rather than dark
     no JS             CSS animations run on load by themselves. Nothing
                       here can fail to start, and reduced motion removes
                       the whole thing rather than shortening it
   ====================================================================== */

/* ---------- the light coming up behind ---------- */
.ignite{
  position: fixed;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  background:
    radial-gradient(58% 46% at 50% 48%, rgb(var(--accent-rgb) / .38), transparent 72%),
    radial-gradient(100% 90% at 50% 50%, rgb(var(--accent-rgb) / .12), transparent 78%);
  animation: igniteFlare 1.6s cubic-bezier(.2,.7,.3,1) both;
}

@keyframes igniteFlare{
  0%   { opacity: 0; }
  9%   { opacity: 1; }
  34%  { opacity: .45; }
  100% { opacity: 0; }
}

/* ---------- the wards ----------
   Pinned to the same rectangle as the screen's cut corners, so they are
   struck around the reading area rather than around the window. Both
   dimensions stated: an SVG is a replaced element, and an auto dimension is
   taken from the viewBox instead of from the offsets. */
.wards{
  position: fixed;
  left:   max(clamp(.8rem, 3vw, 2rem), calc(50% - 26rem));
  top:    clamp(.8rem, 3vh, 2rem);
  right: auto;
  bottom: auto;
  width:  min(52rem, calc(100vw - 2 * clamp(.8rem, 3vw, 2rem)));
  height: calc(100vh - 2 * clamp(.8rem, 3vh, 2rem));
  height: calc(100dvh - 2 * clamp(.8rem, 3vh, 2rem));
  z-index: 7;                       /* over the glass: they are between you
                                       and the page, which is the point */
  pointer-events: none;
  color: rgb(var(--accent-rgb) / .9);
  stroke-width: 1;
  animation: wardStrike 2.1s cubic-bezier(.2,.7,.3,1) both;
}

/* Struck slightly large and settling in, so they close on the page rather
   than growing out of it — a ward is placed over a thing, not grown from it. */
@keyframes wardStrike{
  0%   { opacity: 0; transform: scale(1.055); }
  11%  { opacity: .95; transform: scale(1.02); }
  38%  { opacity: .6;  transform: scale(1); }
  72%  { opacity: .22; }
  100% { opacity: 0;   transform: scale(1); }
}

/* ---------- the leaf, coming up ----------
   Held dark only briefly, then lit. The delay is short on purpose: this is
   an eight-thousand-word read and the opening must never feel like a wait. */
.log-page .volume{
  animation: leafIlluminate .95s ease-out both;
}

@keyframes leafIlluminate{
  0%   { opacity: 0; }
  22%  { opacity: .12; }
  100% { opacity: 1; }
}

/* the glass takes the flare too, so the pane looks lit rather than the page
   glowing behind an inert sheet */
.glass{ animation: glassCatch 1.8s ease-out both; }

@keyframes glassCatch{
  0%   { opacity: 0; }
  14%  { opacity: 1; }
  100% { opacity: 1; }
}

/* ---------- reduced motion ----------
   Removed outright rather than shortened. Someone who has asked for less
   motion has not asked for a faster flash. */
@media (prefers-reduced-motion: reduce){
  /* the turn still needs to register, so it fades rather than swinging */
  .leaf.is-current,
  .leaf.is-current.turn-fwd,
  .leaf.is-current.turn-back{
    animation: leafFade .18s ease both;
    transform: none;
  }
  @keyframes leafFade{ from { opacity: 0; } to { opacity: 1; } }

  .turn-sheet{ display: none !important; animation: none; }
  .ignite{ display: none; }
  .wards{ display: none; }
  .log-page .volume{ animation: none; }
  .glass{ animation: none; }
}

/* ======================================================================
   KRAKUL — the changed hand, and the rime

   Utol drinks the Quickener in the last minutes of the last entry and the
   Glow reaches him as he runs, so the hand changes for four paragraphs and
   no more. It is the only place in the file, which is why it lands.

   The change is cold, not shaky. A wobbling hand reads as fear; this man is
   not afraid, he is calcifying — "my joints begin to stiffen", "the Glow
   calcifies my skin". So the writing goes brittle rather than unsteady: the
   letters are put down out of true, in hard steps rather than a smooth
   waver, and the strokes themselves break up as the nib skips over a
   surface that has started to crystallise.

   The ink does NOT glow. An earlier pass haloed it with text-shadow and
   tracked the letters apart, and both were wrong: tracked-out body text
   reads as broken typography rather than as a failing hand, and the glow
   belongs to the dust on the page, not to a light coming off the writing.

   Four steps so it worsens visibly rather than all at once — and what
   escalates is the PROPORTION of letters that have gone off as much as the
   amount, because a deteriorating hand puts more and more of the line down
   wrong rather than putting every letter down slightly wrong from the
   start.
   ====================================================================== */

.log p.hand-glow{
  transition: none;

  /* ---- the strokes breaking up ----
     The nib skipping over a surface that has begun to crystallise. Cut at
     -8deg rather than flat: a horizontal repeat over body text reads as a
     scanline, and .glass already puts one of those on this page.

     A mask and not a filter. Gecko often rasterises filters on the CPU and
     this is two thousand characters of body text — the same reasoning that
     keeps drop-shadow off the album page's animated paths.

     #fff for the opaque stops, not #000. mask-mode resolves to alpha here,
     where either works; but under a luminance interpretation #000 is zero
     luminance and the mask inverts, hiding the paragraph outright. #fff is
     correct under both readings, so it cannot fail that way. */
  -webkit-mask-image: repeating-linear-gradient(-8deg,
    #fff 0 var(--nib-on),
    transparent var(--nib-on) calc(var(--nib-on) + var(--nib-off)));
          mask-image: repeating-linear-gradient(-8deg,
    #fff 0 var(--nib-on),
    transparent var(--nib-on) calc(var(--nib-on) + var(--nib-off)));
}

.log p.hand-1{ color: #d7d6c6; --nib-on: 10px;  --nib-off: .7px; }
.log p.hand-2{ color: #c6cfc9; --nib-on: 8px;   --nib-off: 1px;  }
.log p.hand-3{ color: #b4c8d1; --nib-on: 6.5px; --nib-off: 1.4px; }
.log p.hand-4{ color: #a6c4d4; --nib-on: 5.5px; --nib-off: 1.8px; }

/* ---- the letters going off true ----
   effects.js does the wrapping, because a character needs a box of its own
   before it can be moved. That split is deliberate: with scripting off the
   writing still goes cold and still breaks up, it merely stops being
   misplaced, and the paragraph reads perfectly well either way.

   The displacement is a transform, so it cannot disturb the leading. The
   word carries nowrap because an atomic inline box opens a line-break
   opportunity beside it, and without this a word could break in half. */
.log p.hand-glow .hand-word{ white-space: nowrap; }

.log p.hand-glow .hand-char{
  display: inline-block;
  transform: translate(var(--hx), var(--hy)) rotate(var(--hr));
}

/* ---------- while a leaf is turning ----------
   Every per-file treatment stands down for the length of a turn.

   This cannot be done with z-index. The turn sheet is inside .volume, which
   is a stacking context at z-index 1, so it can never paint above a layer
   that sits on the body at z 3 — and both treatments have one there, by
   design, because it has to be over the ink. The result was the dust film
   and the oil sheen lying flat on top of a page swinging in 3D, which is
   precisely the "on your screen rather than on the paper" reading that the
   whole layering exists to prevent.

   Out fast and back slowly: the turn has already started when the class
   lands, so the exit must not lag it, while the return can afford to be
   gentle because by then the sheet has cleared and the leaf is settled.

   .rime is deliberately NOT in this list. The burst is meant to be over
   everything, a turn included — and krakul's backstop can fire it ON a
   turn, so hiding it here would sometimes swallow the whole beat. */
/* The treatment is WIPED IN by the page passing over it, rather than hidden
   for the turn and put back afterwards. Hiding and restoring was correct but
   it read as the effect loading late however short the gap was made — because
   an effect that arrives after the page has settled is, plainly, late. Coming
   in behind the moving sheet is not a delay at all: it is the page being
   turned to reveal what is on it.

   The sweep must follow the sheet. Forward hinges on its LEFT and swings the
   free edge away, so it uncovers from the RIGHT; back hinges right and
   uncovers from the left. Hence two directions rather than one.

   A mask rather than a clip so it costs nothing at rest: the mask is only
   declared inside the .is-turning-* rules, so outside a turn these layers
   carry no mask at all.

   THE MASK IS SIZED TO THE COLUMN, NOT TO THE ELEMENT. That is the thing to
   preserve here. These canvases span the viewport while the treatment only
   exists in the 46rem column, so anything expressed as a percentage of the
   element makes the ramp far wider than the content and turns the wipe into
   a cross-fade. See the note on mask-size below. */
.is-turning-fwd .rime-page,
.is-turning-fwd .rime-film,
.is-turning-fwd .damp,
.is-turning-fwd .sheen,
.is-turning-back .rime-page,
.is-turning-back .rime-film,
.is-turning-back .damp,
.is-turning-back .sheen{
  /* 96rem, not a percentage of the element. These canvases are the full
     viewport but the treatment only exists inside the 46rem reading column,
     so a mask sized to the element sweeps mostly across empty canvas — and,
     far worse, its soft ramp came out at 1098px against a 634px column, so
     the whole column sat inside the fade at once and cross-faded instead of
     wiping. Direction was invisible, which is why reversing it changed
     nothing anyone could see. Sized to the column, the ramp is ~123px
     against those 634 and it reads as an edge travelling across. */
  -webkit-mask-size: 96rem 100%;
          mask-size: 96rem 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* THE TWO TURNS ARE NOT MIRROR IMAGES, AND THAT IS THE WHOLE POINT HERE.
   sheetFwd starts flat and rotates to opacity 0, so it UNCOVERS the leaf.
   sheetBack starts edge-on at opacity 0 and ends flat and opaque, so it
   COVERS it — and is then taken away in one go when the class drops. Reading
   them as mirrored is what put the sweep the wrong way round: on a back turn
   the page is closing over the leaf, and the treatment was arriving while it
   did, which is precisely backwards.

   So forward reveals in the sheet's wake, and back is concealed in step with
   the sheet closing, reappearing at the instant the sheet itself vanishes.

   forward: uncovered from the right, so the opaque run sits at the mask's
   far end and travels leftward into the layer */
.is-turning-fwd .rime-page,
.is-turning-fwd .rime-film,
.is-turning-fwd .damp,
.is-turning-fwd .sheen{
  -webkit-mask-image: linear-gradient(270deg, #000 0 50%, rgba(0,0,0,0) 58%);
          mask-image: linear-gradient(270deg, #000 0 50%, rgba(0,0,0,0) 58%);
  animation: wetSweepFwd .30s cubic-bezier(.42,.02,.32,1) both;
}

/* back: THE FORWARD SWEEP RUN BACKWARDS, not the mirror of it.

   Those are different things and the difference is the whole of this fix. A
   mirrored sweep also conceals, but it takes the page from the other side, so
   going back does not undo what going forward did - it does something new in
   the opposite corner. Reversed, the two are one movement: forward lays the
   treatment on from the right, and back lifts it off the way it was laid.

   Hence the SAME gradient as forward with the keyframes reversed. The easing
   is NOT reversed, for the reason given on .turn-sheet.flip-back: a reversed
   settle reads as a dawdle. Duration follows sheetBack, because that is the
   edge it has to stay with. */
.is-turning-back .rime-page,
.is-turning-back .rime-film,
.is-turning-back .damp,
.is-turning-back .sheen{
  -webkit-mask-image: linear-gradient(270deg, #000 0 50%, rgba(0,0,0,0) 58%);
          mask-image: linear-gradient(270deg, #000 0 50%, rgba(0,0,0,0) 58%);
  animation: wetSweepBack .34s cubic-bezier(.42,.02,.32,1) both;
}

/* Measured from the centre, because the column is centred and the canvas is
   not. The opaque run is the mask's right 48rem, so its leading edge sits at
   X + 48rem: at X = 50% - 17rem that edge is clear to the right of the column
   (1456 vs 1277 at 1920 wide, ramp included), and at X = 50% - 68rem the run
   covers it entirely (640..1408 over 643..1277). Holds at any width — checked
   at 1920, 1280 and 400. */
@keyframes wetSweepFwd{
  from { -webkit-mask-position: calc(50% - 17rem) 0; mask-position: calc(50% - 17rem) 0; }
  to   { -webkit-mask-position: calc(50% - 68rem) 0; mask-position: calc(50% - 68rem) 0; }
}

/* wetSweepFwd exactly backwards: the same endpoints, taken the other way.
   With the opaque run at the mask's far end (270deg) P=100 covers the element
   and P=0 leaves it, so this runs visible -> hidden with the hidden part
   growing from the LEFT - the reverse of forward, which revealed with the
   visible part growing from the right. */
@keyframes wetSweepBack{
  from { -webkit-mask-position: calc(50% - 68rem) 0; mask-position: calc(50% - 68rem) 0; }
  to   { -webkit-mask-position: calc(50% - 17rem) 0; mask-position: calc(50% - 17rem) 0; }
}

/* Fallback for an engine with no mask support: hide it for the turn, which
   is what this did before the sweep and is still better than a flat overlay
   riding on a page swinging in 3D. */
@supports not ((mask-image: linear-gradient(#000, #000)) or
               (-webkit-mask-image: linear-gradient(#000, #000))){
  .is-turning .rime-page,
  .is-turning .rime-film,
  .is-turning .damp,
  .is-turning .sheen{ opacity: 0; }
}

@media (prefers-reduced-motion: reduce){
  /* No sweep. The turn itself is already a fade here, and the treatment is
     the document rather than a flourish, so it simply stays. */
  .is-turning-fwd .rime-page,
  .is-turning-fwd .rime-film,
  .is-turning-fwd .damp,
  .is-turning-fwd .sheen,
  .is-turning-back .rime-page,
  .is-turning-back .rime-film,
  .is-turning-back .damp,
  .is-turning-back .sheen{
    animation: none;
    -webkit-mask-image: none;
            mask-image: none;
  }
}

/* ---------- MAREYN: the wet page ----------
   The riftstorm is over; the mark it left is not. Two layers, and they sit
   either side of the writing for a physical reason: water darkens paper, so
   the wetness is UNDER the ink, while light coming off a wet surface is on
   the near side of it. The display case is nearer still, so both stay below
   .glass and its sheen falls across them.

   Body-level z-index on these pages runs .damp 0, .volume 1, .sheen 3,
   .glass 5, .ignite 6, .wards 7, .rime 8 — check that list before adding
   another layer. Both canvases are made by effects.js, not by the builder. */
.damp,
.sheen{
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

.damp { z-index: 0; }
.sheen{ z-index: 3; }

@media (prefers-reduced-motion: reduce){
  /* The page stays wet — that is the document, and it does not move. What
     goes is the shimmer, which is the only part of this that is motion. */
  .sheen{ display: none; }
}

/* ---------- the ward, holding it ----------
   The wards strike on load and fade away. This brings them back to do the
   thing a ward is actually for: the burst goes off inside the order's
   containment and the containment holds it.

   Lifted above .rime while it holds, so the sigils sit between you and the
   blast rather than being buried under it — which is the whole image. */
.wards.is-holding{
  z-index: 9;
  animation: wardHold 3.3s cubic-bezier(.2,.7,.3,1) both;
}

@keyframes wardHold{
  0%   { opacity: 0;   transform: scale(1.03); }
  6%   { opacity: 1;   transform: scale(1); }
  40%  { opacity: .85; }
  62%  { opacity: .55; }
  100% { opacity: 0;   transform: scale(1); }
}

/* ---------- the rime ----------
   Three layers, and the reason they are three is that they want opposite
   z-order. The dust is ON THE PAGE and the burst is ON YOU, so no single
   canvas can hold both: whichever depth it took would be wrong for one of
   them. effects.js creates the two dust layers; only .rime is in the markup.

     .rime-page   0   the heavy smear, beneath the ink AND beneath .glass,
                      so the sheen and the scanlines fall across it. That is
                      most of what makes it read as being in the paper.
     .rime-film   3   a light film over the ink, still under the glass.
     .rime        8   the burst, over the glass, the flare and the wards.
                      The ending has to leave the page; a burst held inside
                      the text column would be an effect on a document.

   Body-level z-index here runs .volume 1, .rime-film 3, .glass 5, .ignite 6,
   .wards 7, .rime 8 — check that list before introducing another layer. */
.rime,
.rime-page,
.rime-film{
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

.rime-page{ z-index: 0; }
.rime-film{ z-index: 3; }
.rime     { z-index: 8; }

@media (prefers-reduced-motion: reduce){
  /* The hand still changes and the dust still lies on the page — both are
     the document, not decoration, and neither of them moves. What goes is
     the one thing that is actually motion: three hundred shards thrown at
     the screen. */
  .rime{ display: none; }
}
