/*
 * One stylesheet, every page. No build step and no framework: the files here
 * are the files that get uploaded.
 *
 * Every value comes from tokens.css. There is no raw colour and no arbitrary
 * length in this file, and a test enforces it rather than trusting it.
 *
 * Load order matters - tokens.css is its own <link> rather than an @import
 * from here, because an @import is a serial fetch.
 */

/* ---------- reset, kept to what is needed ---------- */

*, *::before, *::after { box-sizing: border-box; }
body, h1, h2, h3, h4, p, ul, ol, dl, dd, figure, pre, table { margin: 0; }
ul, ol { padding: 0; }

html {
    background: var(--ground);
    -webkit-text-size-adjust: 100%;
    scrollbar-color: var(--line) var(--ground);
}

body {
    font-family: var(--type-text);
    font-size: var(--step-0);
    font-weight: 400;
    line-height: var(--leading);
    color: var(--ink);
    background: var(--ground);
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

/* The surfaces the browser draws, which belong to the design too. */
::selection { background: var(--accent); color: var(--ground); }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--ground); }
::-webkit-scrollbar-thumb { background: var(--line); }
::-webkit-scrollbar-thumb:hover { background: var(--ink-quiet); }
input, textarea { caret-color: var(--accent); }

:where(a) {
    color: var(--link);
    text-decoration-thickness: 1px;
    text-underline-offset: 0.22em;
}
:where(a):hover { color: var(--accent); }

:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: var(--radius);
}

img, svg { max-width: 100%; height: auto; display: block; }

.skip {
    position: absolute;
    left: -9999px;
    top: 0;
    background: var(--accent);
    color: var(--ground);
    padding: var(--space-2xs) var(--space-sm);
    z-index: 10;
    font-weight: 500;
}
.skip:focus { left: 0; color: var(--ground); }

/* ---------- masthead ---------- */

/* The navigation sits on --ground with a single rule under it, which is the
   one structural borrowing from the reference: elevation is a step in COLOUR,
   never a shadow, so the bar is separated by a line and by what is under it
   rather than by a cast edge. */
.masthead {
    border-bottom: 1px solid var(--line);
    background: color-mix(in srgb, var(--ground) 88%, transparent);
    backdrop-filter: blur(8px);
    position: sticky;
    top: 0;
    z-index: 5;
}

.wrap { width: min(100% - var(--space-md), var(--page)); margin-inline: auto; }

/* The masthead and the footer are bars rather than columns: they run edge to
   edge with a gutter, so the article can be as narrow as reading wants without
   the navigation being squeezed into the same width. */
.masthead__inner {
    width: min(100% - var(--space-md), var(--page));
    margin-inline: auto;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs) var(--space-md);
    padding-block: var(--space-xs);
}

/* The mark is the site's only drawn asset, so it is allowed to be a mark
   rather than a decoration: four EGA squares at a size you can actually see. */
.brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    text-decoration: none;
    color: var(--ink);
    margin-right: auto;
}
.brand:hover { color: var(--ink); }
.brand__mark {
    width: 2.75rem;
    height: 2.75rem;
    flex: none;
}
.brand__text { display: flex; flex-direction: column; line-height: 1.1; }
.brand__name {
    font-size: var(--step-1);
    font-weight: 700;
    letter-spacing: -0.015em;
}
.brand__sub {
    font-family: var(--type-text);
    font-size: var(--step--1);
    font-weight: 400;
    color: var(--ink-quiet);
    letter-spacing: 0;
}

/* ---------- nav ---------- */

.nav__groups {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
    font-size: var(--step--1);
    margin: 0;
    padding: 0;
    max-width: none;
}
.nav__groups > li { margin: 0; }

.menu { position: relative; }

.menu summary,
.nav__direct {
    color: var(--ink-dim);
    text-decoration: none;
    padding-block: var(--space-3xs);
    border-bottom: 1px solid transparent;
    white-space: nowrap;
    font-weight: 500;
}
.menu summary {
    cursor: pointer;
    list-style: none;
    user-select: none;
    display: inline-flex;
    align-items: center;
    gap: var(--space-3xs);
}
.menu summary::-webkit-details-marker { display: none; }
.menu summary:hover,
.nav__direct:hover { color: var(--ink); }
.menu details[open] > summary { color: var(--ink); border-bottom-color: var(--line); }
.menu summary .icon { transition: transform 160ms cubic-bezier(0.2, 0, 0, 1); }
.menu details[open] > summary .icon { transform: rotate(180deg); }

/* The group holding the current page stays marked even when shut. */
.menu details[data-current] > summary,
/* THE CURRENT, and this is its whole job: the thing you are looking at now.
   The ember asks; this reports. Keeping them apart is what stops either from
   becoming decoration. */
.nav__direct.is-current {
    color: var(--ink);
    /* The current runs under the page you are on. A border cannot take a
       gradient, so it is a background image pinned to the bottom edge - which
       also lets it be 2px without moving the text, as a thicker border would. */
    border-bottom-color: transparent;
    background-image: var(--current);
    background-repeat: no-repeat;
    background-position: bottom left;
    background-size: 100% 2px;
}

/* Focus is the current too, everywhere. One answer to "where am I" rather than
   a different ring per control. */
a:focus-visible,
button:focus-visible,
summary:focus-visible {
    outline: 2px solid var(--link);
    outline-offset: 2px;
    border-radius: var(--radius);
}

.menu__items {
    list-style: none;
    margin: 0;
    padding: var(--space-3xs);
    position: absolute;
    top: calc(100% + var(--space-2xs));
    left: calc(var(--space-2xs) * -1);
    min-width: 19rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: 0 12px 32px -8px var(--ground);
    z-index: 20;
}
.menu__items li { margin: 0; }
.menu__items a {
    display: block;
    padding: var(--space-2xs) var(--space-xs);
    text-decoration: none;
    color: var(--ink);
    border-radius: var(--radius);
}
.menu__items a:hover,
.menu__items a:focus-visible { background: var(--ground); }
.menu__items a[aria-current="page"] { color: var(--accent); }
.menu__label { display: flex; align-items: baseline; gap: var(--space-2xs); font-weight: 500; }
.menu__wip {
    font-family: var(--type-text);
    font-size: var(--step--1);
    letter-spacing: 0;
    color: var(--accent);
    border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--line));
    border-radius: 999px;
    padding: 0 var(--space-2xs);
    white-space: nowrap;
}
.menu__blurb {
    display: block;
    color: var(--ink-quiet);
    font-size: var(--step--1);
    margin-top: var(--space-3xs);
}

/* The masthead's one call to action. */
.nav__play {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    text-decoration: none;
    white-space: nowrap;
    /* The same control as .cta, at the masthead's size: the same ember, the
       same corner, the same face. It had a square corner and monospace left
       over from an earlier button, so the one button in the masthead and the
       two on the homepage were visibly different things (29 Sep 2026). */
    background: var(--ember);
    border: 0;
    border-radius: var(--radius);
    box-shadow: var(--shadow-inset);
    padding: var(--space-3xs) var(--space-sm);
    line-height: 1.5;
    font-family: var(--type-text);
    font-size: var(--step--1);
    font-weight: 500;
    /* Ground, not ink, on the ember: white on it measured 2.4 to 4.0. */
    color: var(--ground);
}
.nav__play b { font-weight: 700; }
.nav__play:hover { color: var(--ground); filter: brightness(1.12); }

.icon { width: 1em; height: 1em; flex: none; fill: none; stroke: currentColor;
        stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }

/* ---------- page ---------- */

main { flex: 1 0 auto; padding-block: var(--space-xl) var(--space-2xl); }

/* One column, and everything in it fills the column: prose, tables, pictures.
   Nothing stops short of the right edge, because a paragraph that wraps two
   thirds of the way across its own container looks broken rather than
   considered.

   This was `display: contents`, and that broke every anchor on the site (found
   18 September 2026). A section carries the id the contents list points at, and
   an element with `display: contents` generates no box - so there is nothing to
   scroll to, `scroll-margin-top` has nothing to apply to, and the browser stays
   where it is. Ten pages had a dead "On this page", and the footer's
   /legal#takedown - the link a rights holder is told to use - landed at the top
   of the page rather than on the section naming the address.

   A plain block does the same job here. `.wrap` is what sets the column width;
   a block-level section inside it already fills that column edge to edge, which
   is all the rule was ever buying. */
main > .wrap > section { display: block; }

/* Anchor clearance goes on whatever carries the id, because that is what the
   links target. On the heading it left the masthead covering the first line. */
[id] { scroll-margin-top: var(--space-xl); }

/* Sections are divided by a rule rather than by space alone, so a long page reads
   as a set of parts. The heading sits at the top of its part, not floating in the
   gap between two of them. */
section > h2 { margin-top: 0; }
main > .wrap > section + section {
    border-top: 1px solid var(--line);
    padding-top: var(--space-md);
}
/* The first section sits under the contents list rather than under another
   section, so it takes the smaller gap. This used to be carried by the h2 rule
   alone, because the section's own margin was inert; now that the section is a
   block the two collapse together and the larger wins, so the section has to
   say it too. */
main > .wrap > section:first-of-type { margin-top: var(--space-md); }

.hero { margin-bottom: var(--space-md); }
h1 {
    font-size: clamp(var(--step-3), 5.5vw, var(--step-4));
    line-height: var(--leading-tight);
    letter-spacing: -0.022em;
    font-weight: 700;
    text-wrap: balance;
}
.standfirst {
    font-size: var(--step-1);
    line-height: 1.45;
    color: var(--ink-dim);
    margin-top: var(--space-sm);
    text-wrap: pretty;
}
.standfirst + .standfirst { margin-top: var(--space-xs); font-size: var(--step-0); }

section { margin-top: var(--space-md); }

h2 {
    font-size: var(--step-2);
    line-height: var(--leading-tight);
    letter-spacing: -0.016em;
    font-weight: 700;
    margin-bottom: var(--space-sm);
    text-wrap: balance;
}
h3 {
    font-size: var(--step-1);
    line-height: 1.25;
    letter-spacing: -0.008em;
    font-weight: 600;
    margin-top: var(--space-md);
    margin-bottom: var(--space-2xs);
    text-wrap: balance;
}
/* No max-width on prose: the column is the measure. */
p { text-wrap: pretty; }
/* Flow: every block in a part is a step below the one before it. This was a list of
   adjacent pairs (p + p, p + ul, ...) and the list kept missing pairs - the swatches
   on /hidden sat touching the paragraph under them. One rule, and the two kinds of
   heading say their own space. */
p + p, p + ul, p + ol, ul + p, ol + p, p + pre, pre + p, p + table, table + p,
p + .table-scroll, .table-scroll + p,
main section > * + * {
    margin-top: var(--space-sm);
}
main section > h3 { margin-top: var(--space-md); }
main section > h2 + * { margin-top: 0; }
ul, ol { padding-left: 1.3em; }
li + li { margin-top: var(--space-3xs); }
li::marker { color: var(--ink-quiet); }

strong { font-weight: 600; color: var(--ega-15); }
em { font-style: italic; color: var(--ink); }

/* ---------- code and data ---------- */

/* Quiet, not cyan. Inline code was a bordered cyan pill, the same colour as a link,
   and a paragraph carrying five of them read as five things to click (critique of
   13 October 2026). A tint on the raised step is enough to say "this is a value". */
code {
    font-family: var(--type-mono);
    font-size: 0.875em;
    font-variant-ligatures: none;
    background: var(--raised);
    border: 0;
    border-radius: var(--radius);
    padding: 0.05em 0.3em;
    color: var(--ink-dim);
    white-space: nowrap;
}
kbd {
    font-family: var(--type-mono);
    font-size: 0.875em;
    font-weight: 500;
    line-height: 1;
    padding: 0.15em 0.45em;
    border: 1px solid var(--line);
    border-bottom-width: 2px;
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    white-space: nowrap;
}

pre {
    /* ELEVATION IS A STEP IN COLOUR. --ground, --surface, --raised and --shell
       are a ladder and a panel goes up it; nothing here casts a shadow. That is
       the one structural rule taken from the reference system, and it is the
       reason the violet undertone has to run through all four - a neutral grey
       step on a violet ground reads as a different material. */
    background: var(--raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: var(--space-sm);
    overflow-x: auto;
    font-size: var(--step--1);
    line-height: 1.55;
    font-family: var(--type-mono);
    font-variant-ligatures: none;
}
pre code {
    background: none;
    border: 0;
    padding: 0;
    color: var(--ink);
    font-size: inherit;
    white-space: pre;
}
/* Inline code is nowrap on purpose, so an offset like 0x13C50 never breaks
   across two lines. On a phone that rule was stronger than the viewport:
   img-ega-raw-planar-bgri-fullscreen is 34 characters with nowhere to break,
   and it gave /prior-art a horizontal scrollbar at 420px. Below 40rem the
   token may break rather than push the page sideways. A short one still will
   not, because it fits. Block code keeps its own scroller: a listing should
   scroll, not reflow. */
@media (max-width: 40rem) {
    :not(pre) > code { white-space: normal; overflow-wrap: anywhere; }
}

pre .c { color: var(--ink-quiet); }   /* comment */
pre .a { color: var(--accent); }      /* address */

/* ---------- tables ---------- */

.table-scroll {
    overflow-x: auto;
    margin-top: var(--space-sm);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

table {
    border-collapse: collapse;
    font-size: var(--step-0);
    width: 100%;
    min-width: 30rem;
    font-variant-numeric: tabular-nums;
}
caption {
    text-align: left;
    color: var(--ink-quiet);
    font-family: var(--type-text);
    font-size: var(--step--1);
    padding: var(--space-2xs) var(--space-xs);
    border-bottom: 1px solid var(--line);
}
th, td {
    text-align: left;
    padding: var(--space-2xs) var(--space-xs);
    border-bottom: 1px solid var(--line);
    vertical-align: top;
}
tbody tr:last-child td { border-bottom: 0; }
thead th {
    background: var(--shell);
    color: var(--accent);
    font-weight: 600;
    font-family: var(--type-text);
    font-size: inherit;
    letter-spacing: 0;
    white-space: nowrap;
}
td code, th code, caption code { background: none; border: 0; padding: 0; font-size: inherit; }
td kbd { font-size: inherit; }

/* ---------- callouts ---------- */

.note {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
    padding: var(--space-sm);
    margin-top: var(--space-md);
}
.note > :first-child { margin-top: 0; }
.note h3 { margin-top: 0; }
.note--warn { border-color: color-mix(in srgb, var(--danger) 40%, var(--line)); }
.note--good { border-color: color-mix(in srgb, var(--positive) 40%, var(--line)); }

/* The one first-person passage on the site. Everything else is written
   impersonally, so this is set apart rather than run in: a rule on the left,
   larger type, and a name under it. Dan's decision, 16 September 2026 - the
   motive is his, the findings are the project's. */
.motive {
    margin: var(--space-md) 0;
    padding: 0 0 0 var(--space-sm);
    border-left: 2px solid var(--accent);
}
.motive p {
    font-size: var(--step-1);
    line-height: 1.45;
    color: var(--ink);
    margin: 0;
    text-wrap: pretty;
}
.motive cite {
    display: block;
    margin-top: var(--space-xs);
    font-family: var(--type-text);
    font-size: var(--step--1);
    font-style: normal;
    letter-spacing: 0.04em;
    color: var(--ink-quiet);
}

/* ---------- on this page ---------- */

/* A panel one step up the ladder, so the contents read as a thing of their own
   between the standfirst and the first part, rather than two loose rules. */
.toc {
    margin-top: var(--space-md);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: var(--space-sm) var(--space-md);
}
.toc h2 {
    font-family: var(--type-text);
    font-size: var(--step--1);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-quiet);
    margin-bottom: var(--space-2xs);
}
.toc ul {
    list-style: none;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: var(--space-3xs) var(--space-md);
    font-size: var(--step--1);
}
.toc li { margin: 0; }
.toc a { color: var(--ink-dim); text-decoration: none; }
.toc a:hover { color: var(--accent); text-decoration: underline; }

/* ---------- the index on the home page ----------
   A ruled list rather than a grid of equal cards: these nine pages are not
   nine equal things, and a card grid says they are. */

.index { list-style: none; padding: 0; margin-top: var(--space-md); max-width: none; }
.index li { margin: 0; border-top: 1px solid var(--line); }
.index li:last-child { border-bottom: 1px solid var(--line); }
.index a {
    display: grid;
    grid-template-columns: minmax(12rem, 20rem) 1fr;
    align-items: baseline;
    gap: var(--space-xs) var(--space-md);
    padding: var(--space-sm) var(--space-2xs);
    text-decoration: none;
    color: var(--ink);
}
.index a:hover { background: var(--surface); }
.index h3 { margin: 0; font-size: var(--step-1); }
.index a:hover h3 { color: var(--accent); }
.index h3 { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2xs); }
.index p { margin: 0; color: var(--ink-dim); }
@media (max-width: 40rem) {
    .index a { grid-template-columns: 1fr; gap: var(--space-3xs); }
}

/* ---------- the ten worlds, on the homepage ---------- */

.worlds {
    list-style: none;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: var(--space-sm);
    margin-top: var(--space-sm);
}
@media (max-width: 64rem) { .worlds { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 40rem) { .worlds { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-xs); } }
.worlds li { margin: 0; }
.worlds a {
    display: flex;
    flex-direction: column;
    gap: var(--space-3xs);
    text-decoration: none;
    color: var(--ink);
}
.worlds img {
    width: 100%;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
    transition: border-color 160ms cubic-bezier(0.2, 0, 0, 1);
}
.worlds a:hover img { border-color: var(--link); }
.worlds a:hover .worlds__name { color: var(--accent); }
.worlds__name {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2xs);
    margin-top: var(--space-3xs);
    font-weight: 600;
}
.worlds__meta { font-size: var(--step--1); color: var(--ink-quiet); }
.worlds__credit { margin-top: var(--space-sm); font-size: var(--step--1); color: var(--ink-quiet); }

/* ---------- status pills ---------- */

.pill {
    display: inline-block;
    font-family: var(--type-text);
    font-size: var(--step--1);
    font-weight: 600;
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 0 var(--space-2xs);
    color: var(--ink-quiet);
    vertical-align: middle;
}
.pill--wip { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); color: var(--accent); }
.pill--gated { border-color: color-mix(in srgb, var(--danger) 45%, var(--line)); color: var(--danger); }

/* ---------- the palette, which is the subject ---------- */

.palette {
    display: grid;
    grid-template-columns: repeat(16, 1fr);
    gap: 2px;
    margin-top: var(--space-sm);
}
.palette div { aspect-ratio: 1; border-radius: 1px; }

.swatches { margin-top: var(--space-sm); }
.swatch-row {
    display: grid;
    grid-template-columns: 6rem 1fr;
    gap: var(--space-sm);
    align-items: center;
    margin-bottom: var(--space-3xs);
}
.swatch-row span {
    font-family: var(--type-mono);
    font-size: var(--step--1);
    color: var(--ink-dim);
}
.swatch-row .bar { display: grid; grid-template-columns: repeat(16, 1fr); gap: 1px; height: 1.6rem; }
.swatch-row .bar i { display: block; }
/* The sixteen EGA colours as classes. The swatches carried them as inline
   style attributes, and the site's Content-Security-Policy does not allow
   inline styles, so every one of the hundred and sixty was dropped and the
   palette bars on /hidden rendered empty (found 29 Sep 2026). */
.bar .e0  { background: var(--ega-0); }   .bar .e1  { background: var(--ega-1); }
.bar .e2  { background: var(--ega-2); }   .bar .e3  { background: var(--ega-3); }
.bar .e4  { background: var(--ega-4); }   .bar .e5  { background: var(--ega-5); }
.bar .e6  { background: var(--ega-6); }   .bar .e7  { background: var(--ega-7); }
.bar .e8  { background: var(--ega-8); }   .bar .e9  { background: var(--ega-9); }
.bar .e10 { background: var(--ega-10); }  .bar .e11 { background: var(--ega-11); }
.bar .e12 { background: var(--ega-12); }  .bar .e13 { background: var(--ega-13); }
.bar .e14 { background: var(--ega-14); }  .bar .e15 { background: var(--ega-15); }
@media (max-width: 40rem) {
    .swatch-row { grid-template-columns: 4.5rem 1fr; gap: var(--space-2xs); }
}

/* ---------- sequence ---------- */

.seq {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    justify-content: space-between;
    border-top: 1px solid var(--line);
    margin-top: var(--space-2xl);
    padding-top: var(--space-md);
}
.seq a {
    text-decoration: none;
    color: var(--ink);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: var(--space-2xs) var(--space-sm);
    min-width: 15rem;
    font-weight: 500;
}
.seq a:hover { border-color: var(--accent); color: var(--accent); }
.seq span {
    display: block;
    font-family: var(--type-text);
    font-size: var(--step--1);
    font-weight: 600;
    color: var(--ink-quiet);
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.seq__next { text-align: right; margin-left: auto; }

/* ---------- footer ---------- */

.footer {
    border-top: 1px solid var(--line);
    padding-block: var(--space-lg);
    font-size: var(--step--1);
    color: var(--ink-quiet);
}
/* Left-aligned, on the masthead's edges, and full width like every other
   paragraph on the site. The byline and the legal text were centred across
   78rem - three and four lines of centred prose that no eye can track. */
.footer p + p { margin-top: var(--space-2xs); }
.footer a { color: var(--ink-dim); }
.footer a:hover { color: var(--accent); }
.footer__small strong { color: var(--ink-dim); font-weight: 500; }

.footer__index {
    display: grid;
    gap: var(--space-md);
    grid-template-columns: repeat(4, minmax(0, 1fr));
    margin-bottom: var(--space-lg);
}
@media (max-width: 56rem) {
    .footer__index { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 34rem) {
    /* Two columns, not one: the four groups in a single column ran 600 pixels on a
       phone before the footer said anything. */
    .footer__index { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-sm) var(--space-xs); }
}
.footer__index h2 {
    font-family: var(--type-text);
    font-size: var(--step--1);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-quiet);
    margin-bottom: var(--space-2xs);
}
.footer__index ul { list-style: none; padding: 0; }
.footer__index li { margin: 0 0 var(--space-3xs); }
.footer__index a { text-decoration: none; }
.footer__index a:hover { text-decoration: underline; }


/* ---------- narrow ---------- */

/* Intermediate widths: tighten before stacking. */
@media (max-width: 64rem) {
    .nav__groups { gap: var(--space-xs); }
    .menu__items { min-width: 16rem; }
}
@media (max-width: 56rem) {
    .brand__sub { display: none; }
    .nav__gated span { display: none; }
    .nav__gated { padding: var(--space-2xs) var(--space-xs); }
}

/* A phone. The brand takes the first row and the navigation the second, as
   one wrapped row of labels rather than a column. The column put the three
   menus and Play down the right-hand side beside the brand, 170 pixels of
   masthead before a word of the page (29 Sep 2026). An open menu drops as a
   full-width panel under the row, anchored to the nav, so opening one never
   pushes its neighbours sideways. */
@media (max-width: 44rem) {
    .masthead { position: static; }
    .nav { width: 100%; position: relative; }
    .nav__groups { gap: var(--space-2xs) var(--space-sm); }
    .menu { position: static; }
    .menu__items {
        left: 0;
        right: 0;
        min-width: 0;
        top: calc(100% + var(--space-3xs));
    }
    .toc { padding: var(--space-sm); }
    .seq a { min-width: 0; width: 100%; }
    .seq__next { text-align: left; }
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        transition-duration: 0.001ms !important;
    }
}


/* ---------- the play landing ----------
   The only page on this site where a picture of the running game appears -
   VIP's own art, and a pair from the faithful build that shows the 1988 game as
   it shipped, credited under it. content.test.mjs holds both halves of that. */

.shots {
    margin: var(--space-sm) 0;
    display: grid;
    gap: var(--space-2xs);
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
    /* Capped rather than full-bleed (Dan, 16 Sep 2026). This used to take the
       whole 78rem page width on the grounds that a screenshot is the subject
       rather than an illustration beside the prose. With one picture on the
       page that argument produced a 1248px title screen standing over two
       paragraphs, which read as a splash rather than as evidence. 40rem is
       about half the source image's 1280, so it still renders sharp on a
       2x display.
       The grid survives for when the gameplay screenshot arrives: a second
       image pairs with the first inside the same cap rather than reflowing
       the section. */
    max-width: 40rem;
}
.shots figcaption {
    grid-column: 1 / -1;
    color: var(--ink-quiet);
    font-size: var(--step--1);
}
.shots img {
    width: 100%;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--surface);
}
/* The line under a call to action, carrying what used to be jammed into the
   sentence the button sat in: "Open VIP, which will ask for the password" put a
   comma against the right edge of a filled rectangle, which reads as a rendering
   fault rather than as punctuation (Dan, 16 Sep 2026). A button is a label, not
   a clause. Both builds now get the same treatment - they are equivalent, and
   one of them having a button while the other had an inline link was saying
   otherwise. */
.cta__note {
    margin-top: var(--space-2xs);
    color: var(--ink-quiet);
    font-size: var(--step--1);
}

.shots__note {
    color: var(--ink-quiet);
    font-family: var(--type-text);
    font-size: var(--step--1);
}

/* THE EMBER LIVES HERE AND NOWHERE ELSE.
   
   One gradient, one job. It marks the two things on this site a visitor can
   actually do - open a build - and it means nothing the moment it also appears
   on a heading, a rule or a card. The rest of the interface is lit by colour
   steps and by the cold current.
   
   It replaces a hairline yellow box with a hard offset shadow, which read as a
   DOS dialog. That was the right idea when everything was neutral black; on a
   violet ground the fire says "press this" without having to quote anything. */
.cta {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    background: var(--ember);
    color: var(--ground);
    text-decoration: none;
    font-family: var(--type-text);
    font-size: var(--step-0);
    font-weight: 650;
    padding: var(--space-2xs) var(--space-sm);
    border: 0;
    border-radius: var(--radius);
    box-shadow: var(--shadow-inset);
}
.cta:hover { color: var(--ground); }
.cta:hover {
    /* Brighter, not different. A hover that changes the colour of a gradient
       button loses the one thing the gradient was carrying. */
    filter: brightness(1.12);
}
.cta:focus-visible {
    outline: 2px solid var(--link);
    outline-offset: 3px;
}

/* A status line sits under the hero on anything unfinished. Someone arriving
   from a search result never sees the menu's badge. */
.status {
    margin-top: var(--space-sm);
    padding: var(--space-2xs) var(--space-sm);
    border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line));
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink-dim);
    font-size: var(--step--1);
}
.status strong { color: var(--accent); font-weight: 600; }

/* ---------- the homepage hero ---------- */

.hero__cta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
    margin-top: var(--space-md);
}
/* The secondary of a pair: the same box, the same face and size, with a
   hairline in place of the ember. It kept the ember fill and only dimmed its
   text, so the pair read as two identical buttons, one of them faded - neither
   the primary action nor a second option. The fire is for the one thing to do
   first; the outline is still plainly a button. */
.cta--quiet {
    background: transparent;
    box-shadow: inset 0 0 0 1px var(--line);
    color: var(--ink);
    font-weight: 500;
}
.cta--quiet:hover { color: var(--ink); }
.cta--quiet:hover {
    filter: none;
    background: var(--surface);
    box-shadow: inset 0 0 0 1px var(--ink-quiet);
}

.hero__art { margin-top: 0; margin-bottom: var(--space-xl); }

/* The homepage hero: words on the left, the title art on the right, both in the
   first screen. Full width under the words, the picture stood 780 pixels tall
   and pushed every word of the page below the fold. */
.hero--split { margin-bottom: 0; }
.hero--split .hero__art { margin: var(--space-md) 0 0; }
@media (min-width: 60rem) {
    .hero--split {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        gap: var(--space-lg);
        align-items: center;
    }
    .hero--split .hero__art { margin: 0; }
}
.hero__art img {
    width: 100%;
    border: 1px solid var(--line);
    border-radius: var(--radius);
}
.hero__art figcaption {
    margin-top: var(--space-2xs);
    font-size: var(--step--1);
    color: var(--ink-quiet);
}

/* ---------- the analytics notice ----------

   A DOS dialog (Dan, 16 Sep 2026). This is a site about a 1988 DOS game, and
   the analytics notice is the one place the interface can quote its subject
   without getting in the way of it. The previous version was a plain dark card
   and looked like every other cookie bar on the web.

   NOT MODAL (30 Sep 2026). It was a centred <dialog> opened with showModal(),
   asking before anything was counted. Analytics is now on by default with an
   opt-out, so there is nothing to agree to before reading: it is an <aside>
   docked at the bottom of the screen, with no backdrop and no focus trap, and
   the page behind it scrolls and clicks as normal. The DOS look is the shape,
   not the modality, so all of it survives.

   Everything here is period except the things that would make it worse. No
   scanline filter, no CRT curve, no typing animation. The reference is in the
   shape, not in an effect.

   The blue is --dialog, the tenth interface role, which is EGA blue arriving
   through the system rather than around it. See tokens.css.

   Both buttons stay identical in size, weight, padding and border. The only
   difference between them is the word. An opt-out that reads as less
   clickable than OK is not a simple way to object, and this pattern is easy to
   break by "tidying" one of them. Do not. */
.consent {
    position: fixed;
    z-index: 60;
    left: var(--space-sm);
    bottom: var(--space-sm);
    /* Room above for the title, which sits in the top rule, and to the right
       and below for the hard shadow. */
    width: min(54ch, calc(100% - 2 * var(--space-sm) - 10px));
    margin: 0;
    padding: 0 0 var(--space-sm);
    background: var(--dialog);
    /* The double rule is the dialog. 3px double gives two hairlines with a gap,
       which is what a box-drawing character was doing on a text-mode screen. */
    border: 3px double var(--ink);
    border-radius: 0;
    /* Hard offset, no blur: a text-mode shadow was a block of black cells, not
       a gradient. --ground rather than a literal, per the colour rule. */
    box-shadow: 10px 10px 0 var(--ground);
    color: var(--ink-dim);
    font-family: var(--type-mono);
    font-size: var(--step--1);
    line-height: var(--leading);
}
.consent[hidden] { display: none; }
.consent:focus { outline: none; }
.consent:focus-visible { outline: 2px solid var(--link); outline-offset: 4px; }

/* The title sits in the top rule rather than under it, which is the detail that
   makes the reference land. Pulled up by half its own line and given the
   dialog's background so it knocks a hole in the border it sits on. */
.consent h2 {
    width: max-content;
    max-width: calc(100% - var(--space-md));
    margin: -0.72em auto var(--space-sm);
    padding: 0 var(--space-2xs);
    position: relative;
    top: -0.1em;
    background: var(--dialog);
    color: var(--ink);
    font-size: var(--step-0);
    font-weight: 600;
    letter-spacing: 0.06em;
    line-height: var(--leading-tight);
    text-align: center;
}
/* The box-drawing rules that make the title sit IN the border. In CSS rather
   than in the markup so a screen reader hears the title and not six equals
   signs - the "" after the slash is the alt text, which is what stops them
   being announced - and so the breakpoint below can drop them on a phone,
   where the box is narrower than the decorated title would be. */
.consent h2::before { content: "═══ " / ""; }
.consent h2::after { content: " ═══" / ""; }
.consent p {
    margin: 0 var(--space-sm) var(--space-2xs);
    color: var(--ink-dim);
}
.consent a { color: var(--link); white-space: nowrap; }
.consent .consent__status { color: var(--ink); font-weight: 600; }
.consent__status[hidden] { display: none; }

.consent__actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-md);
    margin-top: var(--space-sm);
}
/* Not .cta: these sit on the dialog's blue, not on the page, so they take the
   ground as their fill and the ink as their rule. `font: inherit` matters: a
   <button> does not inherit the page's font on its own, and one on the estate
   rendered with no visible label in a headless check. */
.consent__btn {
    min-width: 10ch;
    min-height: 44px;
    background: var(--ground);
    color: var(--ink);
    border: 1px solid var(--ink);
    border-radius: 0;
    padding: var(--space-3xs) var(--space-sm);
    box-shadow: 3px 3px 0 var(--ground);
    font: inherit;
    font-size: var(--step--1);
    cursor: pointer;
}
.consent__btn:hover,
.consent__btn:focus-visible {
    background: var(--accent);
    color: var(--ground);
    border-color: var(--accent);
}
.consent__btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* On a phone the box runs the width of the screen, which is narrower than the
   decorated title. So the rules go, the title falls back to plain text, and the
   two buttons sit side by side at equal width. It is still a DOS dialog: the
   double border, the shadow and the monospace all survive. */
@media (max-width: 34rem) {
    .consent {
        left: var(--space-2xs);
        bottom: max(var(--space-2xs), env(safe-area-inset-bottom));
        width: calc(100% - 2 * var(--space-2xs) - 6px);
        box-shadow: 6px 6px 0 var(--ground);
    }
    .consent h2::before,
    .consent h2::after { content: none; }
    .consent__actions {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: var(--space-2xs);
        margin-inline: var(--space-sm);
    }
    .consent__btn { width: 100%; min-width: 0; }
}

/* "Cookie settings" in the footer reopens the notice. A button, because it
   does something rather than going somewhere, drawn as the footer's links are
   drawn so it reads as one of them. Hidden until consent.js runs. */
.footer__settings[hidden] { display: none; }
.footer__settings button {
    padding: 0;
    background: none;
    border: 0;
    color: var(--ink-dim);
    font: inherit;
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}
.footer__settings button:hover { color: var(--accent); }


/* ---------- the caves, drawn whole ---------- */

/* figure.shot was in the markup of every cave map and styled nowhere, so twenty
   1600-pixel images ran the full 78rem column one under another: a 9,000 pixel
   page for a set of twenty things that want comparing.

   Two up is not an arbitrary halving. A cave is 40 tiles across; half of a
   78rem column is about 600 pixels, which is 15 pixels a tile against the
   original's 16. So the pair renders at very nearly the size the game drew it,
   and a world's four caves sit in one screen as a set. */
.caves {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-sm);
    margin-top: var(--space-sm);
}

/* Two, fixed, rather than auto-fit. A world holds exactly four caves, and
   auto-fit put three across a 1440 screen and left the fourth alone on a row
   of its own - which reads as a fifth thing that failed to load. Two by two is
   the shape of the set. */
@media (max-width: 48rem) {
    .caves { grid-template-columns: 1fr; }
}

.shot { margin: 0; }

.shot img {
    width: 100%;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
    /* These are tile art sampled down from eight times size, not a pixel grid,
       so they are smoothed rather than pixelated - the opposite of what the
       sprite strips want. */
}

/* Body size, not caption size. The site's smaller step is for asides, and
   these are not asides: the quota, the clock and the two scoring rates are the
   only facts on the page that are not a picture. Set a step down they read as
   footnotes to the image, and they sit directly under a 17px paragraph saying
   far less, which is what made the page look like two different documents.
   The quieter colour still keeps them subordinate to the prose. */
.shot figcaption {
    margin-top: var(--space-2xs);
    color: var(--ink-quiet);
    font-size: var(--step-0);
    line-height: 1.5;
}
.shot figcaption strong { color: var(--ink); }

/* ---------- the sprite directory ---------- */

/* A four-column table whose first cell holds a 480 pixel strip cannot stay a
   table on a phone. At 390 the page measured 609 and took the whole document
   sideways with it, which put "Object" and "What it does" off the screen -
   the two columns the page exists for.

   The site's other tables solve this with .table-scroll, and that is the wrong
   answer here: a horizontal scroller inside a vertical list of 26 rows means
   reading every row twice. Above the breakpoint this stays a table, because it
   is one. Below it, each row becomes a card in the order a narrow screen can
   take: the picture, then what the thing is. */
/* The site's tables are 14px because they are data - byte offsets, hex values,
   columns you scan rather than read. This one is prose in a table: a paragraph
   per row explaining what an object does, and it is the whole point of the
   page. At 14px it sat three pixels under the paragraph introducing it, which
   is what made the page read as two documents. So this table alone takes the
   body size. */
.sprites { min-width: 0; font-size: var(--step-0); }

.sprites img {
    width: 100%;
    max-width: 30rem;
    border-radius: var(--radius);
}

.sprites td:first-child { width: 42%; }
/* Two strips to a row: the five 1988 worlds over the five hidden ones. */
.sprites__art img + img { margin-top: var(--space-2xs); }
.key { margin-top: var(--space-md); }
.key p + p { margin-top: var(--space-2xs); }
.sprites td:nth-child(2), .sprites td:nth-child(3) { white-space: nowrap; }

@media (max-width: 52rem) {
    .sprites, .sprites tbody, .sprites tr, .sprites td, .sprites th { display: block; width: auto; }

    /* The header is still read aloud in a table context by some screen readers,
       so it is hidden visually rather than removed. Pinned to the table's own
       top-left: absolutely positioned with no offsets it keeps its static
       position, which is a 1px box past the right edge of a 390px viewport and
       shows up as document overflow even though it is clipped. */
    .sprites { position: relative; }
    .sprites thead {
        /* A table-header-group ignores `overflow`, so the row inside kept its
           full 344px and reached past the right edge of a 390px viewport -
           clipped nowhere, just not painted anywhere useful. As a block it is
           clipped for real. */
        display: block;
        position: absolute;
        top: 0; left: 0;
        width: 1px; height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .sprites tr {
        border: 1px solid var(--line);
        border-radius: var(--radius);
        background: var(--surface);
        padding: var(--space-xs);
        margin-bottom: var(--space-xs);
    }
    .sprites tbody tr:last-child { margin-bottom: 0; }
    .sprites tbody tr:hover { background: var(--surface); }

    .sprites td { border-bottom: 0; padding: var(--space-3xs) 0; }
    .sprites td:first-child { width: auto; }
    .sprites img { max-width: 100%; }

    /* Value and name are one line on a card rather than two rows of a table
       that is no longer there. */
    .sprites td:nth-child(2), .sprites td:nth-child(3) { display: inline-block; }
    .sprites td:nth-child(2) { margin-right: var(--space-2xs); }
}


/* ---------- the document layout: contents beside the text ----------

   The page stays 78rem with the same small margins (Dan, 29 September 2026: no
   squashing). What changes on a wide screen is how the width is used. Prose ran
   the full 1248 pixels at about 150 characters a line, twice what reading wants,
   and a 10,000 pixel page had its contents list only at the top. So on document
   pages the contents move into a column of their own beside the text, stay in view
   and mark the part being read, and the text column comes down to about 95
   characters. Galleries (/caves, /sprites, the homepage) keep the full width,
   because their pictures are the subject: <main class="wide">. */

@media (min-width: 75rem) {
    main:has(> .wrap > .toc):not(.wide) > .wrap {
        display: grid;
        grid-template-columns: minmax(0, 1fr) 16rem;
        column-gap: var(--space-xl);
        align-items: start;
    }
    main:has(> .wrap > .toc):not(.wide) > .wrap > * { grid-column: 1; min-width: 0; }
    main:has(> .wrap > .toc):not(.wide) > .wrap > .hero { grid-column: 1 / -1; }
    main:has(> .wrap > .toc):not(.wide) > .wrap > .toc {
        grid-column: 2;
        grid-row: 2 / span 80;
        position: sticky;
        top: var(--space-xl);
        max-height: calc(100vh - var(--space-xl) - var(--space-md));
        overflow-y: auto;
        margin-top: var(--space-md);
        padding: 0 0 0 var(--space-sm);
        background: none;
        border: 0;
        border-left: 1px solid var(--line);
        border-radius: 0;
    }
    main:has(> .wrap > .toc):not(.wide) > .wrap > .toc ul {
        display: block;
        font-size: var(--step--1);
    }
    main:has(> .wrap > .toc):not(.wide) > .wrap > .toc li + li { margin-top: var(--space-2xs); }
    main:has(> .wrap > .toc):not(.wide) .standfirst { max-width: none; }
}

/* The part being read, marked in the contents by nav.js. */
.toc a[aria-current="true"] { color: var(--accent); }
.toc a { display: inline-block; line-height: 1.35; }

/* A link to each part, added by nav.js, so a part can be cited. Hidden until the
   heading is pointed at or focused, so it does not sit in every heading. */
.anchor {
    margin-left: var(--space-2xs);
    color: var(--ink-quiet);
    opacity: 0;
    text-decoration: none;
    vertical-align: 0.1em;
    transition: opacity 120ms cubic-bezier(0.2, 0, 0, 1);
}
.anchor .icon { width: 0.7em; height: 0.7em; display: inline-block; }
h2:hover .anchor, h3:hover .anchor, .anchor:focus-visible { opacity: 1; }
.anchor:hover { color: var(--accent); }
@media (hover: none) { .anchor { display: none; } }

/* A wide table scrolls inside its frame on a phone; the edge fades say there is more
   to the side. Painted from the ground so they work on any row; the edge is the
   neutral shell step, where caption text still measures 5.0:1 (a warm glow there
   measured 3.9 and failed). */
.table-scroll {
    background:
        linear-gradient(to right, var(--ground) 30%, transparent) left center / 2rem 100% no-repeat local,
        linear-gradient(to left, var(--ground) 30%, transparent) right center / 2rem 100% no-repeat local,
        linear-gradient(to right, var(--shell), transparent) left center / 0.75rem 100% no-repeat scroll,
        linear-gradient(to left, var(--shell), transparent) right center / 0.75rem 100% no-repeat scroll;
}

/* The footer's two closing paragraphs ran 175 characters a line. Two columns on a
   wide screen, the same margins. */
@media (min-width: 64rem) {
    .footer__small { columns: 2; column-gap: var(--space-xl); }
    .footer__small p { break-inside: avoid; }
}

/* An "in progress" pill beside a 60px heading sat on its baseline. */
h1 .pill { vertical-align: 0.5em; }


/* ---------- phones ----------

   Checked at 360 and 390 pixels wide, 13 October 2026. The masthead took three rows
   and 120 pixels before a word of the page, the page opened 84 pixels further down,
   and a 21-pixel standfirst filled the first screen. */

.nav__short { display: none; }

@media (max-width: 44rem) {
    /* One row of menus: the short names, so the three and Play fit beside each other. */
    .nav__long { display: none; }
    .nav__short { display: inline; }
    .nav__groups { gap: var(--space-2xs) var(--space-xs); flex-wrap: wrap; }
    .menu summary { gap: 0.125rem; }
    .nav__play { padding: var(--space-3xs) var(--space-xs); }
    .masthead__inner { gap: var(--space-2xs); padding-block: var(--space-2xs); }
    .brand__mark { width: 2.25rem; height: 2.25rem; }

    main { padding-block: var(--space-md) var(--space-xl); }
    h1 { font-size: var(--step-3); }
    .standfirst { font-size: var(--step-0); line-height: var(--leading); }
    .standfirst + .standfirst { font-size: var(--step-0); }
    main > .wrap > section + section { padding-top: var(--space-sm); }

    /* Taps, not clicks: the contents and the footer index are lists of links at
       14 pixels, so each gets a taller hit area without changing how it reads. */
    .toc a, .footer__index a { display: block; padding-block: var(--space-3xs); }
    .toc ul { gap: 0; }
    .footer__index li { margin: 0; }

    /* A table of three columns or fewer fits a phone once it stops insisting on
       30rem; only the wider ones still scroll inside their frame. */
    table:not(:has(tr > :nth-child(4))) { min-width: 0; }
    th, td { padding: var(--space-2xs); }

    .hero__cta .cta { flex: 1 1 auto; justify-content: center; }
    .status { padding: var(--space-2xs) var(--space-xs); }
}


/* ---------- the source link (issue 166) ---------- */

.icon--fill { fill: currentColor; stroke: none; }
.nav__source {
    display: inline-flex;
    align-items: center;
    padding: var(--space-3xs);
    color: var(--ink-dim);
    border-radius: var(--radius);
}
.nav__source:hover { color: var(--ink); }
.nav__source .icon { width: 1.35rem; height: 1.35rem; }
.footer__source { display: inline-flex; align-items: center; gap: var(--space-2xs); }
.footer__source .icon { width: 1rem; height: 1rem; }
/* With the source link the four menus, the icon and Play are one row from 360 up
   only if the gaps close a little on the narrowest phones. */
@media (max-width: 24rem) {
    .nav__groups { column-gap: var(--space-2xs); }
    .nav__source { padding: 0; }
    .nav__source .icon { width: 1.2rem; height: 1.2rem; }
}
.footer__index a.footer__source { display: flex; align-items: center; gap: var(--space-2xs); }


/* ---------- buttons, one size ----------

   Play in the masthead, the ember call to action and its outlined partner were
   three heights and two type sizes, and with the text centred by padding the label
   sat off the middle by a pixel or two depending on the face. One height, one type
   size and one weight, and the label centred by the box rather than by padding. */
.nav__play,
.cta,
.cta--quiet {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--control-height);
    padding: 0 var(--space-sm);
    font-size: var(--control-type);
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
}
.nav__play b { font-weight: inherit; }
