/*
 * The page's own layer. Everything Fristil ships sits in @layer fristil, so anything here wins
 * without a single !important. Colours and spacing come from tokens; nothing is hardcoded
 * except the hazard band's geometry.
 */

:root {
  --font-display: "Michroma", ui-sans-serif, system-ui, sans-serif;
  --font-data: "IBM Plex Mono", ui-monospace, monospace;

  --rule: var(--fs-spacing-px) solid var(--fs-color-neutral-border);
  --rule-strong: var(--fs-spacing-px) solid var(--fs-color-neutral-border-strong);
}

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

html {
  -webkit-text-size-adjust: 100%;
}

/* The canvas colour lives on the root so the grain below can blend against it. */
html {
  background: var(--fs-color-neutral-canvas);
}

body {
  position: relative;
  min-height: 100dvh;
  margin: 0;
  background: transparent;
  color: var(--fs-color-neutral-text);
  font-family: var(--font-data);
  font-size: var(--font-size-s);
  line-height: var(--fs-line-height-default);
}

/* The grain. Printed matter has tooth, and a flat screen does not, so the page lays a scanned
   photocopy texture under everything.
 *
 * It sits BEHIND the content, never over it. An overlay on top would blend with the type as
 * well as the paper and quietly take contrast away from every word on the page; behind the
 * content it touches the canvas and nothing else, and the measured contrast of the text is
 * exactly what the theme generator promised.
 *
 * The tile is light paper with dark toner, which is what the light theme wants. The dark theme
 * inverts it, so the speckle becomes light on dark instead of invisible. */

body::before {
  content: "";
  /* Absolute rather than fixed. A full-viewport layer that blends has to be recomposited on
     every scroll frame, and the texture gains nothing from holding still; paper does not stay
     put while the words slide over it. The body is given a minimum height so the layer still
     covers the screen if the page is ever shorter than the viewport. */
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: url("/assets/grain.webp");
  background-repeat: repeat;
  background-size: 384px 384px;
  mix-blend-mode: multiply;
  /* Kept low on purpose. Multiplying a paper texture over the canvas tints it, and the tint
     comes straight out of the contrast margin the theme generator guaranteed. At 0.25 the light
     canvas settles around 241 of 255, where the brand red still measures about 5.1:1 against it
     rather than the 5.9:1 it has on pure white. Pushing this to 0.45 takes it to roughly 4.6:1,
     which is a pass with nothing left over, and texture is not worth spending a margin on. */
  opacity: 0.25;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body::before {
    filter: invert(1);
    mix-blend-mode: screen;
    opacity: 0.14;
  }
}

:root[data-theme="dark"] body::before {
  filter: invert(1);
  mix-blend-mode: screen;
  opacity: 0.14;
}

/* Texture is the first thing to go when someone has asked for more contrast, and it is not
   worth a byte to someone who has asked for less data. */
@media (prefers-contrast: more), (prefers-reduced-data: reduce), print {
  body::before {
    display: none;
  }
}

/* The sheet. A printed page with margins, not a hero section. */

.sheet {
  container-type: inline-size;
  max-width: 54rem;
  margin-inline: auto;
  padding: var(--fs-spacing-8) var(--fs-spacing-5) var(--fs-spacing-12);
}

@media (min-width: 48rem) {
  .sheet {
    padding: var(--fs-spacing-16) var(--fs-spacing-10) var(--fs-spacing-16);
  }
}

/* Masthead */

.wordmark {
  margin: 0;
  font-family: var(--font-display);
  /* Michroma descends from Eurostile, which is what the 1990 manuals and the rulebooks set
     their headers in. It ships one weight and nothing else, so the browser must not be allowed
     to fake a bold: a synthesised Michroma smears the square counters that are the whole point.
     The mass comes from size instead. */
  font-weight: 400;
  font-synthesis: none;
  /* Sized against the sheet, not the viewport, so the word never breaks across two lines. */
  font-size: clamp(1.75rem, 9.6cqi, 6rem);
  line-height: 0.9;
  letter-spacing: 0;
  color: var(--fs-color-neutral-text-strong);
  text-transform: uppercase;
  overflow-wrap: break-word;
}

/* The slashed O is the mark: a port, a crosshair, the empty set.
   `fill` is nominally a background role, so this pairing is ours to answer for rather than the
   theme generator's: against neutral-canvas it measures 5.3:1 in the dark theme and 5.9:1 in
   the light one. The text roles hold more contrast but wash out to pink and to near-black, and
   at this size the mark has to stay the brand's red. */
.wordmark__mark {
  color: var(--fs-color-accent-fill);
}

/* The display face is for names only. Michroma is a nameplate, not a reading face, and a
   sentence set in it is slower to read for everyone and hardest on the people who already read
   slowest. Prose belongs to the data face. */
.masthead__line {
  margin: var(--fs-spacing-5) 0 0;
  max-width: 38rem;
  font-family: var(--font-data);
  font-size: var(--font-size-m);
  color: var(--fs-color-neutral-text);
  text-wrap: pretty;
}

/* A printed rule with a spot-colour mark at the head of it, the way a press sheet carries its
   registration. Decorative, and nothing but CSS, so there is nothing for a screen reader to
   stumble over. */
.masthead::after {
  content: "";
  display: block;
  height: var(--fs-spacing-0-5);
  margin-top: var(--fs-spacing-8);
  background: linear-gradient(
    to right,
    var(--fs-color-accent-fill) 0 3.5rem,
    var(--fs-color-neutral-border-strong) 3.5rem
  );
}

/* The entity block: a registry record that happens to be true. */

/* Two real columns, not a row of flex items that wrap.
 *
 * A wrapping flex row puts the value on a new line at the row's left edge, so on a phone the
 * longest value, the status line, broke out of the column and sat under its own label instead of
 * beside it. A grid gives the value a column of its own: it still wraps, but every line of it
 * stays inside that column and the labels keep their edge. */

.entity {
  margin: var(--fs-spacing-6) 0 0;
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: var(--fs-spacing-4);
  row-gap: var(--fs-spacing-1);
  font-size: var(--font-size-xs);
}

.entity dt {
  grid-column: 1;
  color: var(--fs-color-neutral-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

.entity dd {
  grid-column: 2;
  margin: 0;
  color: var(--fs-color-neutral-text);
  text-wrap: pretty;
}

/* The plate. A solid bar of ink with the type knocked out of it, corners cut at 45 degrees.
   It replaces a decorative band with something that does structural work: it labels the
   architecture underneath it and says how many nodes are in it. */

.plate {
  --chamfer: 0.85rem;
  display: flex;
  align-items: center;
  gap: var(--fs-spacing-4);
  margin: var(--fs-spacing-16) 0 0;
  padding: var(--fs-spacing-2) var(--fs-spacing-4);
  background: var(--fs-color-neutral-text-strong);
  color: var(--fs-color-neutral-canvas);
  clip-path: polygon(
    0 0,
    calc(100% - var(--chamfer)) 0,
    100% var(--chamfer),
    100% 100%,
    var(--chamfer) 100%,
    0 calc(100% - var(--chamfer))
  );
}

/* The spot mark. A block, not a glyph, so it carries no meaning a reader has to be told. */
.plate::before {
  content: "";
  flex: none;
  width: 0.6em;
  height: 0.6em;
  background: var(--fs-color-accent-fill);
}

.plate__label {
  flex: 1;
  font-family: var(--font-data);
  color: inherit;
}

.plate__meta {
  margin: 0;
  font-size: var(--font-size-xxs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: inherit;
}

/* The architecture. An ordered list, drawn as a ladder of nodes. */

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

.node {
  position: relative;
  display: grid;
  grid-template-columns: 3.5rem 1fr;
  grid-template-areas:
    "index name"
    ".     what"
    ".     tags"
    ".     block";
  column-gap: var(--fs-spacing-4);
  padding: var(--fs-spacing-6) 0;
  border-bottom: var(--rule);
}

@media (min-width: 48rem) {
  .node {
    grid-template-columns: 5rem 1fr;
  }
}

.node__index {
  grid-area: index;
  margin: 0;
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.1em;
  color: var(--fs-color-neutral-text-subtle);
  padding-top: 0.35em;
}

.node__name {
  grid-area: name;
  font-family: var(--font-display);
  font-weight: 400;
  font-synthesis: none;
  font-size: clamp(1.1rem, 4.2cqi, 2rem);
  line-height: 1.05;
  letter-spacing: 0;
  text-transform: uppercase;
}

.node__link {
  color: var(--fs-color-neutral-text-strong);
  text-decoration: none;
}

.node__link:hover {
  color: var(--fs-color-accent-text);
  text-decoration: underline;
  text-decoration-thickness: 0.06em;
  text-underline-offset: 0.12em;
}

.node__link:focus-visible {
  outline: var(--fs-focus-ring);
  outline-offset: var(--fs-spacing-1);
}

/* The arrow is the affordance, and it is decorative: the link already says where it goes.
   It is drawn with borders rather than set as a character, because generated text content is
   folded into the link's accessible name and the glyph's own name is not part of it. */
.node__link::after {
  content: "";
  display: inline-block;
  width: 0;
  height: 0;
  margin-left: 0.32em;
  border-block: 0.3em solid transparent;
  border-inline-start: 0.42em solid var(--fs-color-accent-fill);
  vertical-align: 0.04em;
}

.node__what {
  grid-area: what;
  margin: var(--fs-spacing-3) 0 0;
  max-width: 44ch;
  font-size: var(--font-size-s);
  line-height: var(--fs-line-height-article);
  color: var(--fs-color-neutral-text);
  text-wrap: pretty;
}

.node__tags {
  grid-area: tags;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--fs-spacing-2);
  margin: var(--fs-spacing-4) 0 0;
  padding: 0;
}

/* The plate. Line art from a manual whose key is on some other page.
 *
 * The drawing is ornament and the caption is the whole of what it says, so the image carries no
 * alternative text: describing a decoration part by part wastes a listener's time without
 * telling them anything the caption did not. It is pure black on white, so the dark theme
 * inverts it rather than needing a second file. */

.plate-figure {
  margin: var(--fs-spacing-16) 0 0;
  padding-top: var(--fs-spacing-8);
  border-top: var(--rule);
}

.plate-figure__image {
  display: block;
  width: 100%;
  height: auto;
  mix-blend-mode: multiply;
  opacity: 0.72;
}

.plate-figure__caption {
  margin-top: var(--fs-spacing-4);
  font-size: var(--font-size-xxs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fs-color-neutral-text-subtle);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .plate-figure__image {
    filter: invert(1);
    mix-blend-mode: screen;
    opacity: 0.6;
  }
}

:root[data-theme="dark"] .plate-figure__image {
  filter: invert(1);
  mix-blend-mode: screen;
  opacity: 0.6;
}

/* Colophon */

.colophon {
  margin-top: var(--fs-spacing-12);
  padding-top: var(--fs-spacing-5);
  border-top: var(--rule);
  max-width: 44ch;
  color: var(--fs-color-neutral-text-subtle);
}

/* Preferences the page has to obey. */

/* Reduced motion switches transitions off rather than shortening them to nothing.
 *
 * The widespread `transition-duration: 0.01ms` trick is written for transitions that only move
 * paint around, and it exists so that `transitionend` still fires. It is wrong the moment a
 * transition carries layout: a `grid-template-rows` transition from `0fr` to `1fr` with an
 * effectively zero duration stays stuck on the value it started from, and the block it was
 * opening never opens at all. That turns an animation someone asked not to see into a feature
 * they cannot use. Nothing here listens for `transitionend`, so there is nothing to preserve. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

@media (prefers-contrast: more) {
  :root {
    --rule: var(--fs-spacing-px) solid var(--fs-color-neutral-text);
    --rule-strong: var(--fs-spacing-0-5) solid var(--fs-color-neutral-text-strong);
  }

  .node__link {
    text-decoration: underline;
  }
}

@media print {
  body {
    background: #fff;
    color: #000;
  }

  .sheet {
    max-width: none;
    padding: 0;
  }

  .node__link::after {
    content: " (" attr(href) ")";
    font-size: var(--font-size-xxs);
  }
}

/* ---------------------------------------------------------------------------------------------
 * The interface strip.
 *
 * The page's own operating parameters, where a visitor can see them and change them. They are
 * not an accessibility widget bolted to a corner: they are the rig's configuration, which is
 * what this page is dressed as, and they are the first thing on the sheet for the same reason a
 * manual puts the controls before the contents.
 *
 * Every control starts from what the browser already said the visitor wants, and an override is
 * remembered for the next visit. The whole strip is `hidden` in the markup and revealed by
 * Datastar, so a page without JavaScript never shows a control that cannot work.
 * ------------------------------------------------------------------------------------------ */

.interface {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--fs-spacing-2) var(--fs-spacing-6);
  margin-bottom: var(--fs-spacing-10);
  padding-bottom: var(--fs-spacing-3);
  border-bottom: var(--rule);
  font-size: var(--font-size-xxs);
}

.interface__label,
.ctl__legend {
  margin: 0;
  padding: 0;
  color: var(--fs-color-neutral-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.14em;
}

.interface__label {
  color: var(--fs-color-accent-text);
  margin-inline-end: auto;
}

.ctl {
  display: flex;
  align-items: baseline;
  gap: var(--fs-spacing-2);
  margin: 0;
  padding: 0;
  border: 0;
}

.ctl__opt {
  display: inline-flex;
  align-items: center;
  gap: var(--fs-spacing-1);
  cursor: pointer;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fs-color-neutral-text-subtle);
}

/* The radio itself is kept in the page rather than replaced: it is what makes the group a group
   for a screen reader and for the arrow keys. It is only made small and square. */
.ctl__opt input {
  appearance: none;
  width: 0.6em;
  height: 0.6em;
  margin: 0;
  border: var(--fs-spacing-px) solid var(--fs-color-neutral-border);
  background: transparent;
}

.ctl__opt input:checked {
  background: var(--fs-color-accent-fill);
  border-color: var(--fs-color-accent-fill);
}

.ctl__opt:has(input:checked) {
  color: var(--fs-color-neutral-text-strong);
}

.ctl__opt input:focus-visible {
  outline: var(--fs-focus-ring);
  outline-offset: var(--fs-spacing-1);
}

/* ---------------------------------------------------------------------------------------------
 * The data block.
 *
 * A native <details>, so it opens and closes with no script at all. What Datastar adds is that
 * the open one is in the address bar, that the back button closes it, and that only one stands
 * open at a time.
 * ------------------------------------------------------------------------------------------ */

.block {
  grid-area: block;
  margin-top: var(--fs-spacing-4);
}

.block__summary {
  display: inline-flex;
  border: 0;
  background: transparent;
  font: inherit;
  align-items: center;
  gap: var(--fs-spacing-2);
  width: max-content;
  padding: var(--fs-spacing-1) 0;
  cursor: pointer;
  font-size: var(--font-size-xxs);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--fs-color-neutral-text-subtle);
  list-style: none;
}

/* The marker is drawn, not set as a character, for the same reason the node arrow is: generated
   text joins the summary's accessible name, and the name of a triangle is not part of it. */
.block__summary::before {
  content: "";
  width: 0;
  height: 0;
  border-block: 0.3em solid transparent;
  border-inline-start: 0.42em solid var(--fs-color-accent-fill);
  transition: rotate 120ms ease;
}

.block__summary[aria-expanded="true"]::before {
  rotate: 90deg;
}

.block__summary:hover {
  color: var(--fs-color-neutral-text-strong);
}

.block__summary:focus-visible {
  outline: var(--fs-focus-ring);
  outline-offset: var(--fs-spacing-1);
}

/* Closed is the default, and the class is what opens it. A class rather than `data-show`,
   because `data-show` writes an inline display and would be arguing with this rule every time;
   and closed by default rather than open, so there is no flash of three open blocks before the
   script has had a chance to run. The <noscript> block in the head puts them back for a page
   that will never get a button.
 *
 * The block opens by growing its row from nothing to its content's height. A grid row is the
 * one way to animate to a height nobody measured in advance that works in every browser today;
 * `height: auto` still cannot be transitioned without `interpolate-size`, which Chrome has and
 * the others do not.
 *
 * `visibility` carries the part that matters more than the movement. A block that is merely
 * clipped is still in the accessibility tree and its copy button is still in the tab order, so
 * a keyboard would walk into three closed blocks. Hidden takes it out of both. It is in the
 * transition on purpose: going open it flips immediately, going closed it waits for the height
 * to finish, so nothing vanishes mid-collapse. */

.block__reveal {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  transition:
    grid-template-rows 220ms ease,
    visibility 220ms;
}

.block__reveal.is-open {
  grid-template-rows: 1fr;
  visibility: visible;
}

/* The vertical padding travels with the height. Padding is not content, so clipping the
   content does not remove it, and a closed block would otherwise sit there holding a row open
   at exactly the height of its own margins. The horizontal padding is left alone: it costs no
   height and animating it would only wobble the text. */
.block__body {
  display: grid;
  min-height: 0;
  overflow: hidden;
  gap: var(--fs-spacing-2);
  margin: 0;
  padding: 0 var(--fs-spacing-4);
  border-left: var(--fs-spacing-0-5) solid var(--fs-color-neutral-border);
  font-size: var(--font-size-xs);
  transition:
    padding-block 220ms ease,
    margin-block-start 220ms ease;
}

.block__reveal.is-open .block__body {
  margin-block-start: var(--fs-spacing-3);
  padding-block: var(--fs-spacing-4);
}

.block__body dt {
  grid-column: 1;
  color: var(--fs-color-neutral-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

.block__body dd {
  grid-column: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--fs-spacing-2);
  margin: 0;
  min-width: 0;
}

/* A repository URL has no spaces in it, so without this it refuses to break and runs out past
   the column instead. Flex items also will not shrink below their content unless told. */
.block__body dd a {
  min-width: 0;
  overflow-wrap: anywhere;
}

.block__code {
  font-family: inherit;
  color: var(--fs-color-neutral-text-strong);
  overflow-wrap: anywhere;
}

.block__copy {
  padding: var(--fs-spacing-0-5) var(--fs-spacing-2);
  border: var(--fs-spacing-px) solid var(--fs-color-neutral-border);
  background: transparent;
  color: var(--fs-color-neutral-text);
  font: inherit;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  cursor: pointer;
}

.block__copy:hover {
  border-color: var(--fs-color-accent-fill);
  color: var(--fs-color-neutral-text-strong);
}

.block__copy:focus-visible {
  outline: var(--fs-focus-ring);
  outline-offset: var(--fs-spacing-1);
}

/* An override of the system preference has to be obeyed as firmly as the system preference is. */
:root[data-motion="reduce"] *,
:root[data-motion="reduce"] *::before,
:root[data-motion="reduce"] *::after {
  animation-duration: 0.01ms !important;
  animation-iteration-count: 1 !important;
  transition: none !important;
  scroll-behavior: auto !important;
}
