/* Quake 30 — nin.fan
   Palette lifted verbatim from the Dec 1996 id Software homepage:
   <body bgcolor="#000000" text="#ffffff" link="#ff0000" vlink="#7f7f7f">
   Period look, modern behaviour: no JS, and every texture generated rather
   than shipped. The two Quake faces below are the only binary assets. */

/* ---- Quake faces --------------------------------------------------------
   Both converted from the OTFs in ../quake/assets/fonts to woff2, and self
   hosted: they are small bespoke files with no CDN copy. They live at the top
   of the file rather than beside their first use because @font-face is
   document-scoped — a selector never gated them, and burying them in a page
   section only made that look otherwise.

   Console is the 8px conchars face: full ASCII, monospaced at a flat 1em
   advance. It sets both the store's copy and every heading on the site.

   Menu is the display slab from the game's menu screen. Nothing uses it at
   present — headings moved to Console — but it is kept declared because an
   unreferenced @font-face is never fetched, so it costs a reader nothing and
   makes switching back a one-word change to --q30-display. Note if you do:
   Menu carries just 66 glyphs, A-Z, 0-9 and `- / :`, with no apostrophe,
   comma, period or question mark, so headings would have to avoid all four. */

@font-face {
  font-family: "Quake Console";
  src: url("../fonts/QuakeConsole-Regular.4a3e7a5296b0.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Quake Menu";
  src: url("../fonts/QuakeMenu-Regular.67825ebcda1a.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

.q30 {
  --q30-bg: #000000;
  --q30-text: #ffffff;
  --q30-link: #ff0000;
  --q30-visited: #7f7f7f;

  --q30-rule: #5a5450;

  /* Red and gold, shared by every page. These began as the shopping maul's
     private note-row colours; the store's treatment is now the site-wide one,
     so they moved up here and lost their `maul` prefix. */
  --q30-title-color: #a61315;
  --q30-copy-color: #b29c6d;

  --q30-serif: "Times New Roman", Times, serif;
  --q30-mono: "Courier New", Courier, monospace;
  /* Display face for headings. Console rather than Menu: it is the pixel
     conchars face, it has full ASCII, and headings may therefore carry
     apostrophes, question marks and periods — none of which exist in Menu.
     Anything set in it still wants text-transform: uppercase for even forms.
     Every rule using this must also disable font smoothing; the grouped
     selector below does that, so add new headings there rather than restating
     font-family locally. */
  --q30-display: "Quake Console", "Courier New", Courier, monospace;

  /* Distressed rust-on-black border texture, generated rather than shipped.
     Two filters stacked on a black plate: `rust` maps desaturated turbulence
     through per-channel tables (R climbs fast, G moderately, B barely) to ramp
     black -> burnt orange-brown; `eat` then punches irregular black blotches
     over it at a much lower frequency, which is what reads as "distressed"
     rather than merely "noisy".

     TO SWAP IN A REAL TEXTURE: drop a tileable image in static/img/ and point
     this variable at it with a relative reference, replacing the data URI.

     Do NOT write that example reference out in a comment. collectstatic's
     ManifestStaticFilesStorage rewrites CSS references with a regex that does
     not skip comments, so a commented-out path to a file that does not exist
     fails the production build with a ValueError. */
  --q30-border-tex: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80'%3E%3Cfilter id='rust'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='5' seed='3'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='table' tableValues='0 0.12 0.5 0.82'/%3E%3CfeFuncG type='table' tableValues='0 0.04 0.19 0.38'/%3E%3CfeFuncB type='table' tableValues='0 0.02 0.07 0.14'/%3E%3CfeFuncA type='table' tableValues='1 1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cfilter id='eat'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.12' numOctaves='3' seed='11'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='table' tableValues='0 0'/%3E%3CfeFuncG type='table' tableValues='0 0'/%3E%3CfeFuncB type='table' tableValues='0 0'/%3E%3CfeFuncA type='table' tableValues='0 0 1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='80' height='80' fill='%23000000'/%3E%3Crect width='80' height='80' filter='url(%23rust)'/%3E%3Crect width='80' height='80' filter='url(%23eat)'/%3E%3C/svg%3E");

  min-height: 100vh;
  padding-bottom: 2rem;
  background: var(--q30-bg);
  color: var(--q30-text);
  font-family: var(--q30-serif);
  font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  line-height: 1.5;
  text-align: center;
  overflow-x: hidden;
}

.q30 *,
.q30 *::before,
.q30 *::after {
  box-sizing: border-box;
}

.q30 img {
  max-width: 100%;
}

.q30 a {
  color: var(--q30-link);
}

.q30 a:visited {
  color: var(--q30-visited);
}

.q30 a:focus-visible {
  outline: 2px solid var(--q30-text);
  outline-offset: 2px;
}

.q30-wrap {
  max-width: 60rem;
  margin: 0 auto;
  padding: 1.5rem 1rem 0;
}

/* A banner, not just a heading: a running shambler flanks each side, as on the
   maul. Flex rather than absolute positioning so the three parts shrink
   together and the creatures never overlap the words.

   font-weight stays at the default: the face ships one weight, and asking for
   700 makes a browser synthesise a smeared fake bold from it. The extrusion is
   stated in reds to match the palette rather than the grey stone it was cut
   for. */
.q30-page-title {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: center;
  gap: clamp(0.25rem, 2vw, 1.5rem);
  margin: 0 0 0.5rem;
  font-size: clamp(1.75rem, 7vw, 3rem);
  letter-spacing: 0.06em;
  /* Letter-spacing adds a trailing gap; indent by the same to recentre. */
  text-indent: 0.06em;
  text-transform: uppercase;
  color: #d8d2cc;
  text-shadow: 0 2px 0 #4e0e0e, 0 3px 0 #300808, 0 4px 10px rgba(255, 0, 0, 0.3);
}

/* Smaller than the 141px the maul and altar give them: those flank artwork
   150px tall, whereas this flanks a single line of type. Roughly twice the
   cap height reads as creatures looming over the title; at full size they
   dwarf it. */
.q30-page-title .q30-shambler {
  flex: 0 1 clamp(56px, 12vw, 96px);
}

/* min-width: 0 lets the title shrink below its intrinsic width, so on a narrow
   screen the words give ground with the shamblers instead of forcing the row
   wider than the viewport. */
.q30-page-title > span {
  min-width: 0;
}

.q30-lede {
  margin: 0 auto 1.5rem;
  max-width: 40rem;
  color: #cfc9c3;
}

/* ---- Display headings ---------------------------------------------------
   Every element set in the Quake Console display face, in one place. Grouped
   because the face carries a hard requirement beyond the family name: it is an
   8px bitmap traced to outlines, so antialiasing greys its edges into mush and
   smoothing has to come off wherever it is used. Splitting family and
   smoothing across separate rules is how a heading ends up blurry.

   Letter-spacing stays local to each rule, but note that Console is
   monospaced at a flat 1em advance — every glyph, `I` and `W` alike, occupies
   a full em. It needs far less tracking than the proportional Menu slab this
   replaced, which is why the values here are small. */

.q30-page-title,
.q30-divider,
.q30-news-head,
.q30-counter-label,
.q30-counter-digit,
.q30-altar-years,
.q30-toc-link,
.q30-toc li::marker,
.q30-step h2,
.q30-connect h2,
.q30-step-num,
.q30-platform h3,
.q30-platform h4,
.q30-servers th {
  font-family: var(--q30-display);
  -webkit-font-smoothing: none;
  -moz-osx-font-smoothing: grayscale;
}

/* ---- Cells --------------------------------------------------------------
   Applied alongside a layout class, e.g. class="q30-stone q30-news".

   Flat black, rather than the grey carved-stone gradient this used to be — the
   store proved the black treatment out and it is now the site-wide one, so it
   lives on the base class instead of being overridden per page. The old bevel
   (inset light/dark shadows over a stone gradient) is gone with it; the rust
   border below carries the edge on its own.

   That border is genuinely textured, not a flat colour: border-image is the
   only border property that accepts an image, so it takes an inline SVG of
   desaturated turbulence. border-image-slice carves that tile into the nine
   regions the browser tiles around the edge; `repeat` keeps the grain at a
   constant scale instead of stretching it along long sides. The `border`
   shorthand stays as the fallback colour if the SVG ever fails. */

.q30-stone {
  position: relative;
  background: #000000;
  /* Fallback colour if the texture ever fails to parse. */
  border: 1px solid #6b3a18;
  border-image-source: var(--q30-border-tex);
  /* Slice dropped from 14 to 3 to suit the 1px border. The slice is scaled
     down to the border width, so a 14px slice squeezed into 1px averages the
     noise into a near-flat line and repeats every few pixels. A 3px slice
     survives the reduction with its variation intact. */
  border-image-slice: 3;
  border-image-repeat: repeat;
}

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

.q30-rule {
  margin: 2.5rem auto 1.25rem;
  border: 0;
  border-top: 2px groove var(--q30-rule);
}

/* Flex so the two groups are the units that wrap, rather than five separate
   items breaking wherever the line happens to run out. */
.q30-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  margin: 0 0 0.75rem;
  font-size: 0.95rem;
}

.q30-nav-pages,
.q30-nav-sites {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
}

/* Below 640px the full row no longer fits — it needs roughly 560px — so the
   off-site pair is given a full-width basis, which forces it onto a line of
   its own beneath the three site pages. Its leading separator would then be
   dangling at the start of that line, so it goes. */
@media (max-width: 639px) {
  .q30-nav-sites {
    flex-basis: 100%;
  }

  .q30-nav-sites > .q30-sep:first-child {
    display: none;
  }
}

.q30-nav a,
.q30-nav .q30-here {
  display: inline-block;
  min-height: 44px;
  padding: 0.7rem 0.4rem;
  white-space: nowrap;
}

.q30-nav .q30-here {
  color: var(--q30-text);
  text-decoration: none;
}

.q30-nav .q30-sep {
  color: var(--q30-visited);
}

.q30-copy {
  margin: 0;
  font-size: 0.8rem;
  color: var(--q30-visited);
}

.q30-footer img {
  display: block;
  margin: 1rem auto 0;
}

/* ---- Visitor counter ----------------------------------------------------
   The period hit counter, above the footer nav. Each digit gets its own inset
   box so the row reads as an odometer rather than as a number: individual
   plates on a recessed strip, green on black like the console readout the
   command blocks use.

   Font comes from the grouped display rule above, which also kills smoothing —
   the digits are Console, and the whole point of a fake LCD is hard pixel
   edges. */

.q30-counter {
  margin: 0 0 1rem;
}

.q30-counter-label {
  margin: 0 0 0.4rem;
  font-size: 0.7rem;
  letter-spacing: 0.2em;
  /* Letter-spacing adds a trailing gap; indent by the same to recentre. */
  text-indent: 0.2em;
  text-transform: uppercase;
  color: var(--q30-visited);
}

.q30-counter-readout {
  margin: 0;
}

/* inline-flex so the strip hugs the digits and stays centred by the footer's
   text-align rather than needing a width of its own. */
.q30-counter-boxes {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: #0d0c0b;
  border: 1px solid var(--q30-rule);
}

.q30-counter-digit {
  /* A fixed min-width keeps the plates identical. Console is monospaced, so
     every digit already occupies the same em — this only guards the box
     itself against collapsing narrower than its glyph. */
  min-width: 1.1em;
  padding: 0.2em 0.15em;
  background: #000000;
  color: #66ff66;
  font-size: 1.15rem;
  line-height: 1;
  text-align: center;
}

/* ---- Landing altar ------------------------------------------------------
   Loosely recalls a single 1996 imagemap: one picture with the destinations
   sitting on top of it. The picture is a background rather than an <img>, so
   the links can overlay it without absolute positioning; a grid of named areas
   parks one in each corner and the date across the foot.

   Real anchors rather than <area coords>, which is what buys the responsive
   behaviour — the same corner arrangement holds at every width, the text stays
   selectable and focusable, and there is no fixed pixel canvas to fall off.

   Capped at the artwork's native 640px and centred: past that the photograph
   is upscaled into softness rather than made bigger. aspect-ratio keeps the
   cell the same shape as the picture at any width, so `cover` never has to
   crop anything away. */

.q30-altar {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: auto 1fr auto auto;
  grid-template-areas:
    "tl ."
    ".  ."
    "yr br";
  gap: 0.5rem 1rem;
  width: 100%;
  max-width: 640px;
  aspect-ratio: 640 / 480;
  margin: 0 auto 2rem;
  padding: clamp(0.75rem, 3vw, 1.5rem);
  background: url("../img/quake-main.95e67060bc64.png") center / cover no-repeat, #000000;
}

/* The picture carries no title of its own, so the page heading is here for
   screen readers and search while the artwork does the visible work — as the
   1996 map did, where the labels were the only text. Absolute, so it takes no
   space in the grid. */
.q30-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Two destinations on opposite corners, with the date filling the third so the
   composition is not weighted entirely down one diagonal. */
.q30-altar-tl { grid-area: tl; justify-self: start; }
.q30-altar-br { grid-area: br; justify-self: end; }

.q30-altar-years {
  grid-area: yr;
  justify-self: start;
  margin: 0;
  font-size: 0.95rem;
  letter-spacing: 0.24em;
  /* Letter-spacing adds a trailing gap; indent by the same to recentre. */
  text-indent: 0.24em;
  color: var(--q30-copy-color);
  text-shadow: 0 0 4px #000, 0 0 8px #000, 0 1px 2px #000;
}

/* Labels: red, like the original's burned-in text. The shadow stack does two
   jobs — an opaque black halo first, because these now sit on a photograph
   with pale areas in it and red on light paper is unreadable without one, then
   the ember glow over that so they still look lit rather than outlined.

   The clamp floor is deliberately low: at the narrowest viewports two labels
   share a row inside a 640px-max cell, and the pair has to fit side by side. */
.q30-altar-label {
  /* The Menu slab, not the Console pixel face the rest of the site's headings
     use — this is the game's own menu lettering, which is what two links over
     an image are imitating. Smoothing is explicitly restored: Menu is drawn
     with curves, so the grouped rule's `none` would leave it jagged, and these
     labels are no longer in that group for exactly that reason.

     Both labels are punctuation-free, which Menu requires: its 66 glyphs cover
     A-Z, 0-9 and `- / :` only. */
  font-family: "Quake Menu", "Times New Roman", Times, serif;
  -webkit-font-smoothing: antialiased;
  font-size: clamp(0.9rem, 3.2vw, 1.75rem);
  line-height: 1.15;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  text-decoration: none;
  text-align: center;
  /* Tap target, without a border or background giving the game away. */
  padding: 0.6rem 0.75rem;
  color: var(--q30-link);
  text-shadow:
    0 0 4px #000, 0 0 8px #000, 0 1px 2px #000,
    0 0 14px rgba(255, 40, 0, 0.5);
}

/* Deliberately NOT the period grey vlink: these four are the page's primary
   navigation and grey would gut the composition once they had been clicked.
   The footer's text nav still greys, which is where the era's convention
   actually belongs. */
.q30-altar-label:visited {
  color: var(--q30-link);
}

.q30-altar-label:hover,
.q30-altar-label:focus-visible {
  color: #ff7a4a;
  text-shadow:
    0 0 4px #000, 0 0 8px #000, 0 1px 2px #000,
    0 0 18px rgba(255, 90, 0, 0.75);
}

/* ---- News box ---------------------------------------------------------- */

.q30-news {
  margin: 0 auto 2rem;
  padding: 1rem 1.25rem 1.25rem;
  text-align: left;
}

.q30-news-head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  margin: 0 0 0.75rem;
  font-size: 1.3rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--q30-title-color);
}

.q30-new-badge {
  padding: 0.15em 0.5em;
  background: var(--q30-link);
  color: #ffffff;
  font-family: var(--q30-mono);
  font-size: 0.65em;
  letter-spacing: 0.1em;
  animation: q30-blink 1s steps(1, end) infinite;
}

@keyframes q30-blink {
  50% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .q30-new-badge { animation: none; }
}

.q30-news ul {
  margin: 0;
  padding-left: 1.25rem;
}

.q30-news li {
  margin-bottom: 0.6rem;
}

.q30-news li:last-child {
  margin-bottom: 0;
}

.q30-news time {
  font-family: var(--q30-mono);
  font-size: 0.85em;
  color: var(--q30-visited);
}

/* ---- Shopping maul layout metrics --------------------------------------
   The cell treatment that used to live here (black ground, rust border) is now
   the site-wide .q30-stone, so only the store's own layout metrics remain. */

.q30-maul-page {
  /* Grid gap and the note cell's horizontal padding are variables because the
     note art sizes itself to one product-tile column, and that calculation has
     to reference both. Editing either value here keeps the art in step; a
     literal in one place and not the other would drift it out of alignment. */
  --q30-maul-gap: 0.75rem;
  --q30-maul-note-pad-x: 1.25rem;
}

/* ---- Shopping maul typography ------------------------------------------
   The store runs on Quake Console — the game's own conchars face, converted
   from the OTF in ../quake/assets/fonts to woff2. Self-hosted rather than
   fetched from a CDN because it is a 2KB bespoke file with no CDN copy.

   Scoped to .q30-wrap rather than the whole page because the shared footer is
   a sibling of .q30-wrap, not a child — so it keeps the period serif/Courier
   treatment with no exception rules. Redefining --q30-mono here also carries
   the face into the price and order-button rules, which resolve the variable
   rather than name a font. */

@font-face {
  font-family: "Quake Console";
  src: url("../fonts/QuakeConsole-Regular.4a3e7a5296b0.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

.q30-maul-page .q30-wrap {
  --q30-mono: "Quake Console", "Courier New", Courier, monospace;
  font-family: "Quake Console", "Courier New", Courier, monospace;
  /* Quake Console is an 8px bitmap face traced to outlines: its edges land on
     a pixel grid, and antialiasing greys them into mush. Turning smoothing off
     keeps the blocky conchars silhouette the whole point of using it. */
  -webkit-font-smoothing: none;
  -moz-osx-font-smoothing: grayscale;

  /* Maul type is flat, not fluid. A bitmap face has one drawn size: scaling it
     up on wide viewports magnifies an 8px grid into slabs, so it reads as too
     big long before it reads as too blurry. Every maul size below is the floor
     of the clamp it replaced — the value that rendered in single column, where
     the vw term never won — so wide screens now match narrow instead of
     overshooting them.

     This declaration overrides the fluid base size on .q30 for the store area,
     and stands as the default for anything added here later — every element
     currently in the store sets its own size. The footer is a sibling of
     .q30-wrap, so it keeps the fluid base. */
  font-size: 1rem;
}

/* ---- Shopping maul banner ----------------------------------------------
   Centred header art with a shambler on each side. Above 768px all three
   sit on one line; below it the header takes a full line of its own and the
   two shamblers drop beneath it, still facing each other. */

.q30-maul-banner {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: center;
  gap: clamp(0.25rem, 1.5vw, 1.5rem);
  margin: 0 0 1rem;
}

/* Each element's flex-basis is its natural pixel width, so when space runs
   short all three shrink at the same rate and the composition holds together
   at any viewport — never wrapping the shamblers away from the header.
   min-width: 0 is required or flex items refuse to shrink below intrinsic size. */
.q30-maul-header {
  flex: 0 1 660px;
  min-width: 0;
  height: auto;
}

.q30-shambler {
  flex: 0 1 141px;
  min-width: 0;
  height: auto;
}

.q30-shambler-flip {
  transform: scaleX(-1);
}

/* ---- Shopping maul -----------------------------------------------------
   Six-column grid at desktop: standard items span 2 columns, wide ones span
   3 — which is what makes the bottom row larger. Spans are set by explicit
   classes rather than :nth-child so that adding a row (the note below) does
   not renumber every item and silently break the layout.
   Single column below 768px. */

.q30-maul-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--q30-maul-gap);
  margin: 0 0 1rem;
}

@media (min-width: 768px) {
  .q30-maul-grid { grid-template-columns: repeat(6, 1fr); }
  .q30-maul-item { grid-column: span 2; }
  .q30-maul-item-wide { grid-column: span 3; }
}

/* Full-width note row: left-aligned copy on the left, logo lockup on the
   right. Stays side by side at every width, shrinking together. */

.q30-maul-note {
  grid-column: 1 / -1;
  display: flex;
  /* stretch, not center, so the art can take the full cell height. The copy
     re-centres itself below; only the image wants the stretch. */
  align-items: stretch;
  justify-content: space-between;
  gap: clamp(0.75rem, 3vw, 2rem);
  padding: 1rem var(--q30-maul-note-pad-x);
  text-align: left;
}

/* The copy stacks vertically inside its own flex item, so the two paragraphs
   sit one above the other while the lockup stays alongside them. Without this
   wrapper both paragraphs would be siblings of the image and lay out in a row. */
.q30-maul-note-copy {
  flex: 1 1 auto;
  min-width: 0;
  align-self: center;
}

/* Flat, not fluid — see the sizing note in the maul typography block above.
   1.1rem is the old clamp's floor, i.e. exactly what this rendered at below
   768px, so the single-column look now holds at every width. */
.q30-maul-note-title {
  margin: 0 0 0.5rem;
  font-size: 1.1rem;
  line-height: 1.2;
  color: var(--q30-title-color);
}

/* Product names share this rule with the top cell's copy so the two can't
   drift apart — "same as the top cell" is enforced, not just matched once. */
.q30-maul-note-text,
.q30-maul-name {
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.6;
  color: var(--q30-copy-color);
  /* Backstop: nothing may push past the cell edge, whatever the copy says. */
  overflow-wrap: anywhere;
}

/* ---- Menu cursor beside each product name ------------------------------
   Quake's menu cursor (the spinning sigil from menudot1-6.lmp) sits to the
   left of the name, as it does against the selected entry in the game's own
   menu, so each tile reads as that highlighted row.

   Pseudo-element rather than an <img> tag: this is decoration repeated across
   every product, so the markup would carry five identical tags that the store
   TODO then has to step around when real product data lands. It also keeps
   them out of the accessibility tree for free — no alt="" / aria-hidden
   needed, unlike the shamblers up in the banner, which are one-offs.

   Making the name a flex row does not disturb the <br/> inside it: the text
   and the break are contiguous inline content, so they become a single
   anonymous flex item that still breaks its own line. */

.q30-maul-name {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}

/* The GIF is the original 16x24 asset, drawn at 3x (48x72) and left blocky by
   image-rendering. 3 is deliberately a whole number: nearest-neighbour turns
   each source pixel into an exact 3x3 block with nothing dropped or doubled,
   and a retina display then scales that by a further clean 2. A fractional
   factor would smear the pixel grid no matter what image-rendering says.

   Still the 1x asset, not the 8x copy in ../quake/assets — 128px down to 48px
   is a 2.67x reduction that resamples unevenly and breaks the sigil up, while
   16px up to 48px is exact. Upscaling the small file is the sharper path here.

   flex-shrink is 0 so a long name eats the text column's width rather than
   crushing the cursors; overflow-wrap: anywhere on the name absorbs it. */
.q30-maul-name::before,
.q30-maul-name::after
 {
  content: "";
  flex: 0 0 auto;
  width: 48px;
  height: 72px;
  background: url("../img/quake-menu-cursor.9622a143a21b.gif") center / 48px 72px no-repeat;
  image-rendering: pixelated;
}

/* Back to 2x through the squeeze band. The band's edges are the layout's own,
   not round numbers: 768px is where the grid goes multi-column and the tiles
   first become narrow, and 960px is .q30-wrap's max-width, past which the tiles
   stop growing and are as roomy as they will ever get.

   Inside it a span-2 tile offers roughly 206px of content at 768px, rising to
   266px at 960px. Quake Console is monospaced at a full 1em advance — every
   glyph is exactly 12.8px at this 0.8rem size — so "QUAKE X NIN 30TH" needs a
   flat 204.8px. A 48px cursor plus its 8px gap takes 56px of that; 32px takes
   40px, which is the difference between wrapping and not across the upper half
   of the band. Only width/height/background-size change — image-rendering and
   the URL carry over from the rule above, so 2x stays an exact whole-number
   scale of the same 16x24 source. */
@media (min-width: 768px) and (max-width: 959px) {
  .q30-maul-name::before {
    width: 32px;
    height: 48px;
    background-size: 32px 48px;
  }
}

/* The art is exactly one product-tile column wide, so it reads as part of the
   same column rhythm as the row of tiles below it. Width drives height here,
   via the image's true aspect ratio — the reverse of the old arrangement,
   which stretched the art to the row height and derived width from that. That
   left it sized by however tall the copy happened to wrap (around 224px against
   a 301px tile at full width), so it never matched a column.

   The calc reconstructs a tile's width from the note's own content box. A tile
   spans 2 of 6 columns, so it is 2 track widths plus the one gap between them:

     track = (grid - 5 * gap) / 6      tile = 2 * track + gap

   100% here is the note's content box, which is the full grid width less the
   note's own padding and border — so those are added back to recover `grid`,
   and the whole thing reduces to the expression below. The 2px is the note's
   left and right border, the only literal left in it; gap and padding come
   from the variables so this cannot drift out of step with them.

   flex-shrink is 0: the art holds its column and the copy absorbs any squeeze,
   which it can — it has min-width: 0 and wraps. The old rule let the art shrink
   instead, but that was to break a feedback loop (taller copy -> taller art ->
   wider art -> narrower copy) that no longer exists now width is explicit. */
.q30-maul-note-art {
  flex: 0 0 auto;
  align-self: center;
  width: calc(
    (100% + 2 * var(--q30-maul-note-pad-x) + 2px - 5 * var(--q30-maul-gap)) / 3
    + var(--q30-maul-gap)
  );
  height: auto;
  aspect-ratio: 373 / 150;
  object-fit: contain;
}

/* Single column: stack the cell instead of squeezing copy and art side by
   side. DOM order already puts the copy first, so a column direction alone
   gives text-over-image. The art still fills one column here — there is just
   only one column below 768px, so plain 100% says it and the six-column calc
   above, which has no meaning at this width, is overridden outright. The cap
   keeps it from upscaling past its native 373px on a wide phone. */
@media (max-width: 767px) {
  .q30-maul-note {
    flex-direction: column;
    justify-content: flex-start;
  }

  .q30-maul-note-copy {
    align-self: stretch;
  }

  .q30-maul-note-art {
    align-self: center;
    width: 100%;
    max-width: 373px;
  }
}

.q30-maul-item {
  display: flex;
  padding: 0.75rem;
}

.q30-maul-item > a {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 0.5rem;
  text-decoration: none;
}

.q30-maul-shot {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Square, matching the square product art — so `contain` fits it edge to
     edge with no letterbox bars. */
  aspect-ratio: 1 / 1;
  background: #0d0c0b;
  border: 1px solid #0c0b0a;
  box-shadow: inset 0 0 0 1px #2b2825;
  color: var(--q30-visited);
  font-family: var(--q30-mono);
  font-size: 0.8rem;
  letter-spacing: 0.12em;
}

/* contain, not cover: the whole product stays visible and shrinks to fit the
   slot. cover would fill the slot by cropping whatever overflows — which cut
   the top and bottom off the square box art. */
.q30-maul-shot img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* 0.8rem matches the product name above it, so the whole lower half of the
   tile sits on one size. See the order rule below for why it had to come down
   from the inherited 1rem at all. */
.q30-maul-price {
  font-family: var(--q30-mono);
  font-size: 0.8rem;
  color: var(--q30-text);
}

/* Bare link, not a button: no background, border, or bevel. The brackets sit
   on the outer span and take the gold copy colour; the label inside takes the
   red. The whole tile is already one <a>, so this element no longer needs its
   own 44px tap target.

   Sized down from the inherited 1rem because "[ MORE INFO ]" wrapped below
   ~860px. Quake Console is monospaced at a full 1em advance, so that string
   costs exactly 13 chars x (1em + 0.14em letter-spacing) = 14.82 x font-size,
   with no proportional slack to absorb a squeeze: 237px at 1rem, against the
   235px a span-2 tile offers at 860px. At 0.8rem it needs 190px, which clears
   the narrowest desktop tile (~206px at the 768px breakpoint) with room spare.
   Keep that 14.82 factor in mind before lengthening the label or raising the
   letter-spacing — both push straight back into a wrap. */
.q30-maul-order {
  margin-top: auto;
  padding-top: 0.5rem;
  color: var(--q30-copy-color);
  font-family: var(--q30-mono);
  font-size: 0.8rem;
  letter-spacing: 0.14em;
}

.q30-maul-order-label {
  color: var(--q30-title-color);
}

.q30-maul-item > a:hover .q30-maul-order-label,
.q30-maul-item > a:focus-visible .q30-maul-order-label {
  text-decoration: underline;
}

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

/* Quick connect leads the page, so it is the loudest cell on it: a full gold
   rule rather than the keynote's single red edge. Anyone already set up should
   land, read two lines, and leave. */
.q30-connect {
  margin: 0 0 1.5rem;
  padding: 1.25rem;
  border-color: var(--q30-copy-color);
  /* Overrides the rust texture from .q30-stone — a flat gold edge reads as
     deliberate emphasis where the distressed one reads as ordinary chrome. */
  border-image-source: none;
  text-align: left;
}

.q30-connect > p:first-of-type {
  margin-top: 0;
}

/* Separates the connection details above from the setup guide below. Flanking
   rules use the same `2px groove` as .q30-rule in the footer, so the page has
   one idea of what a horizontal division looks like. */
.q30-divider {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin: 2.5rem 0 1.25rem;
  font-size: clamp(1.2rem, 4.5vw, 1.6rem);
  letter-spacing: 0.08em;
  /* Letter-spacing adds a trailing gap; indent by the same to recentre. */
  text-indent: 0.08em;
  text-transform: uppercase;
  color: var(--q30-title-color);
}

.q30-divider::before,
.q30-divider::after {
  content: "";
  flex: 1;
  border-top: 2px groove var(--q30-rule);
}

/* Terse map of the installation, sitting directly above the steps it indexes.
   A real <ol>, so the numbering is the document's rather than three hand-typed
   digits that drift the moment a step is added or dropped. */
/* Lines up with the cells below it. `inside` puts the numerals in the content
   box rather than hanging them out in the padding, which is what let them
   drift left of the cell edges — with `outside` the marker sits at whatever
   its own width happens to be, so the alignment shifts with the digit count.
   Inside, the padding alone decides where they start, and it is set to the
   cells' own inset: their 1px border plus their 1.25rem padding. */
.q30-toc {
  margin: 0 0 1.5rem;
  padding-left: calc(1.25rem + 1px);
  list-style-position: inside;
  text-align: left;
}

.q30-toc li {
  margin-bottom: 0.5rem;
}

/* ::marker takes the same red as the step numerals it stands in for, so the
   list reads as a smaller echo of the steps below rather than a plain list. */
.q30-toc li::marker {
  color: var(--q30-title-color);
}

/* No rule at rest — these read as a contents list, and three underlined lines
   stacked under the divider look like damage. The underline comes back on
   hover and focus so the affordance is not lost, matching how the maul's
   order labels behave. */
.q30-toc-link {
  letter-spacing: 0.03em;
  text-transform: uppercase;
  text-decoration: none;
}

.q30-toc-link:hover,
.q30-toc-link:focus-visible {
  text-decoration: underline;
}

/* Grey, so the eye lands on the step names first and the qualifiers only when
   it wants them. The platform links inside stay red, because those are the
   second thing anyone is actually hunting for. */
.q30-toc-note {
  color: var(--q30-visited);
  font-size: 0.9rem;
}

/* Anchored sections land clear of the viewport edge instead of flush against
   it, which otherwise hides the heading you just jumped to under the top of
   the window. */
.q30-step,
.q30-platform,
.q30-connect,
.q30-divider {
  scroll-margin-top: 1.5rem;
}

/* The keynote states the buy-versus-play distinction before the steps begin.
   It reads as louder than a step without being a different kind of object: the
   same cell, a red rule down the leading edge, and slightly larger copy.

   That rule is an inset shadow rather than a border-left, because this element
   also carries .q30-stone. Widening one border on a border-image element does
   not give you a thick coloured edge — border-image paints over every side at
   whatever width that side happens to be, so `border-left: 4px solid red` came
   out as a 4px band of the rust texture and the red never appeared at all. An
   inset shadow draws inside the padding box, leaving the 1px rust border
   uniform on all four sides. */
.q30-keynote {
  margin: 0 0 1.5rem;
  padding: 1.25rem 1.25rem 1.25rem 1.5rem;
  box-shadow: inset 4px 0 0 var(--q30-title-color);
  text-align: left;
}

.q30-keynote p {
  margin: 0 0 0.75rem;
  font-size: 1.05rem;
}

.q30-keynote p:last-child {
  margin-bottom: 0;
}

.q30-keynote strong {
  color: var(--q30-copy-color);
}

.q30-step {
  margin: 0 0 1.25rem;
  padding: 1.25rem;
  text-align: left;
}

/* Display face and gold, against the serif prose below it — the contrast is
   the whole point of the split, so headings read as chrome while the
   instructions stay comfortable to read at length. */
.q30-step h2,
.q30-connect h2 {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 0.75rem;
  font-size: clamp(1.1rem, 4vw, 1.35rem);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--q30-copy-color);
}

.q30-step-num {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 1.9em;
  height: 1.9em;
  background: var(--q30-title-color);
  color: #ffffff;
  font-size: 0.85em;
}

/* ol as well as ul: the Steam walkthrough inside step 2 is ordered, and
   without this it keeps the UA's ~40px indent and 1em margins while every
   other list on the page sits at 1.25rem. */
.q30-step ul,
.q30-step ol {
  margin: 0;
  padding-left: 1.25rem;
}

.q30-step li {
  margin-bottom: 0.6rem;
}

/* Numerals in gold, matching the platform headings they sit under, so an
   ordered walkthrough reads as part of its block rather than as body copy. */
.q30-platform ol li::marker {
  color: var(--q30-copy-color);
}

/* Per-platform blocks inside step 3. Indented behind a rule rather than boxed,
   so a reader scanning for their own platform can skip the other two without
   the step fragmenting into three competing cells. */
.q30-platform {
  margin: 1.25rem 0 0;
  padding-left: 1rem;
  border-left: 2px solid #3a2a1c;
}

.q30-platform h3 {
  margin: 0 0 0.5rem;
  font-size: 1rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--q30-copy-color);
}

/* One level down again, for the two store-specific routes inside the
   no-Windows block. Same face, smaller and in the body colour, so it reads as
   a subdivision of the platform block rather than a peer of it. */
.q30-platform h4 {
  margin: 1.25rem 0 0.5rem;
  font-size: 0.9rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--q30-text);
}

/* Optional shortcuts for people who already know their way around. The Windows
   and macOS guides are written for someone who has never opened a terminal, so
   anything requiring one is demoted to this: smaller, grey, and never on the
   path a reader has to follow. */
.q30-aside {
  margin: 0.6rem 0 0;
  font-size: 0.85rem;
  color: var(--q30-visited);
}

/* Gotchas: the failure modes that stop people dead — an unsigned binary macOS
   blocks, a config that silently resets. Red rule and a tint, loud enough to
   catch a skimming eye without becoming a warning icon on a 1996 page. */
.q30-gotcha {
  margin: 0.75rem 0 0;
  padding: 0.6rem 0.75rem;
  background: rgba(166, 19, 21, 0.10);
  border-left: 3px solid var(--q30-title-color);
}

/* Terms stay in the serif and take the gold rather than using the display
   face. Console could set them — it has the apostrophes and question marks
   these need, which Menu did not — but a question is a sentence, and a pixel
   face at body size makes a wall of them tiring to read. The answers beneath
   are serif for the same reason. */
/* The cursor width and the gap after it are variables because the answer is
   indented by exactly their sum, to sit under the start of the question rather
   than under its marker. Changing either here keeps the two in step. */
.q30-faq {
  --q30-faq-cursor-w: 32px;
  --q30-faq-gap: 0.5rem;
  margin: 0;
}

/* Flex so the spinning cursor and the question sit side by side and a question
   that wraps stays clear of the cursor instead of running under it. Centred
   rather than top-aligned: at 2x the sigil is taller than a single line of
   type, so aligning to the top would hang the question off its shoulder. */
.q30-faq dt {
  display: flex;
  align-items: center;
  gap: var(--q30-faq-gap);
  margin: 1.1rem 0 0.35rem;
  color: var(--q30-copy-color);
  font-weight: 700;
}

/* Quake's spinning menu sigil, marking each question the way it marks the
   selected entry in the game's menu. The 16x24 asset drawn at 2x, with
   image-rendering left blocky: 2 is a whole number, so nearest-neighbour turns
   each source pixel into an exact 2x2 block with nothing dropped, and a retina
   display scales that by a further clean 2. flex-shrink is 0 so a long question
   takes the width it needs from the text, never from the sigil. */
.q30-faq dt::before {
  content: "";
  flex: 0 0 auto;
  width: var(--q30-faq-cursor-w);
  height: 48px;
  background: url("../img/quake-menu-cursor.9622a143a21b.gif") center / var(--q30-faq-cursor-w) 48px no-repeat;
  image-rendering: pixelated;
}

.q30-faq dt:first-child {
  margin-top: 0;
}

.q30-faq dd {
  margin: 0;
  padding-left: calc(var(--q30-faq-cursor-w) + var(--q30-faq-gap));
}

.q30-step code,
.q30-step kbd,
.q30-connect code,
.q30-connect kbd {
  font-family: var(--q30-mono);
  color: #cfc9c3;
}

.q30-cmd {
  display: block;
  margin: 0.5rem 0 0;
  padding: 0.6rem 0.75rem;
  overflow-x: auto;
  background: #0d0c0b;
  border: 1px solid var(--q30-rule);
  color: #66ff66;
  white-space: pre;
}

/* Server table: bordered table down to 640px, stacked cards below. */

.q30-servers {
  width: 100%;
  margin: 0.75rem 0;
  border-collapse: collapse;
  text-align: left;
}

.q30-servers th,
.q30-servers td {
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--q30-rule);
}

.q30-servers th {
  background: #1a1512;
  color: var(--q30-copy-color);
  font-size: 0.85rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.q30-servers .q30-addr {
  font-family: var(--q30-mono);
  color: var(--q30-link);
}

@media (max-width: 639px) {
  .q30-servers thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .q30-servers,
  .q30-servers tbody,
  .q30-servers tr,
  .q30-servers td {
    display: block;
    width: 100%;
  }

  .q30-servers tr {
    margin-bottom: 1rem;
    border: 1px solid var(--q30-rule);
  }

  .q30-servers td {
    border: 0;
    border-bottom: 1px solid #2b2825;
  }

  .q30-servers tr td:last-child {
    border-bottom: 0;
  }

  .q30-servers td::before {
    content: attr(data-label);
    display: block;
    font-family: var(--q30-serif);
    font-size: 0.75rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--q30-visited);
  }
}
