/* ===========================================================================
   BRANDING - the one stylesheet for anything carrying the Arkode name.

   The website links this file and nothing else. The dashboard views link it
   too, then widgets.css on top for the parts that only a dashboard has
   (tiles, charts, the matrix). The CV wrapper links it, then CV.css.

   What lives here: the design tokens (the only place a colour is decided),
   the reset, type, links, images, tables, lists, code, the header and footer,
   the reading measure and the section bands, cards, the button, the switch
   (hidden radios, labels as buttons, panes shown by CSS - no script anywhere),
   the chip, print and reduced motion.

   The markdown is the source, the build writes the HTML, this file is the
   look. A script never carries a colour: inline SVG takes var(--green) as a
   fill, a bar's width arrives as a custom property, and the Photo Tagger reads
   the token lines below at start.

   Fonts resolve relative to THIS FILE, so it must sit beside Fonts/ wherever
   it is copied (the site build copies both to /shared).
   ======================================================================== */

@font-face {
  font-family: 'Montserrat';
  src: url('Fonts/montserrat-regular.ttf') format('truetype');
  font-weight: 400;
  font-display: swap;
}

:root {
  /* Surfaces, darkest to lightest - the page, then anything raised off it,
     then anything raised again inside that (quotes, cards, table heads).

     They aren't neutral grey. Each one carries a trace of the brand pink's
     hue, which is what stops a dark site reading as "unstyled black" - the
     eye can't name the tint, but it can tell the difference between a page
     that was coloured and one that just wasn't. */
  --bg: #17161a;
  --band: #232228;      /* every other section, so the page has a rhythm */
  --bg-alt: #2a2830;
  --panel: #322f3a;
  --line: #403c4a;

  /* body text sits a little under white so bold has somewhere to go */
  --text: #d8d5dd;
  --head: #ffffff;
  --muted: #9c98a6;

  /* The brand trio stays, but not at equal weight - three saturated colours
     all shouting is what made the old site restless. Pink is the interface
     colour (links), green is structure (rules, card trim), yellow is reserved
     for focus rings alone, where its 11:1 contrast is an asset.
     --pink-text is the readable version: the brand pink is 2.9:1 on the page
     and fails as body text; this one is 7.9:1. */
  --pink: #C23C8F;
  --pink-text: #EC8FC2;
  --pink-hover: #F7B4D8;
  --green: #4FBF87;
  --yellow: #F1E731;

  /* Rose is the one colour that means "wrong": overdue, blocked, a warning
     tile, money going out. Pink is never used for that, so a pink thing is
     always an interface and a rose thing always a problem. --rose-text is the
     readable version, the way --pink-text is. */
  --rose: #a63d5f;
  --rose-text: #e39ab2;

  /* One radius on everything that can have one; mixed radii read as several
     unrelated widgets rather than one design. */
  --radius: 6px;

  /* the reading measure, ~77 characters. Fixed, NOT in ch: ch is relative to
     each element's own font, so headings would come out wider than the body
     text and every block would start at a different left edge.

     44rem is a compromise, and worth knowing as one. 40rem read at about 70
     characters, which is ideal, but the card grid then sat wider than every
     other block on the page and the misalignment was the thing you noticed.
     Fitting three cards at their old 240px minimum would have needed 47rem,
     or ~82 characters, which is most of the way back to the 95 that made the
     site hard to read in the first place. This holds the line just inside
     comfortable and lets the cards come in narrower to meet it.
     A dashboard is a wall of instruments, not a read, and sets .wide on its
     main to take 72rem instead. */
  --measure: 44rem;
  --gutter: 3rem;

  /* Header geometry, measured not guessed: banner 150 + logo 48 + nav 52.
     --chrome-up is the part allowed to scroll away, --nav-h the strip that
     stays. The breadcrumb parks directly under it. Change the banner height
     and these two are the only numbers that follow. */
  --chrome-up: 198px;
  --nav-h: 54px;        /* header height 252 minus --chrome-up, incl. its border */

  /* Montserrat is a display face - fine for headings, tiring for a long
     read, so body copy uses whatever the reader's system does well */
  --font-head: 'Montserrat', system-ui, sans-serif;
  --font-body: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-mono: Consolas, 'Cascadia Mono', monospace;
}
* { box-sizing: border-box; }
html {
  color-scheme: dark;                 /* scrollbars and form controls match */
}
/* the footer sits at the bottom of the window on a short page instead of
   floating halfway up it */
body {
  margin: 0; min-height: 100dvh;
  display: flex; flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.7;
  overflow-wrap: break-word;          /* a long path can't widen the page */
}
::selection { background: var(--pink); color: #fff; }

/* The jump-to flash is an INSET SHADOW, not a background. Animating the
   background worked on the plain sections and broke the banded ones: the
   keyframe's "transparent" replaced the band's own colour, so a banded
   section flashed and then dropped to the page colour. A shadow lays over
   whatever is underneath and leaves it alone. */
:target { animation: target-flash 1.8s ease-out; }
@keyframes target-flash {
  from { box-shadow: inset 0 0 0 100vmax rgba(236,143,194,.15); }
  to   { box-shadow: inset 0 0 0 100vmax rgba(236,143,194,0); }
}

/* links are underlined everywhere they sit in prose - colour alone was the
   only thing marking them, and it was the weakest colour on the page */
a { color: var(--pink-text); text-decoration: underline; text-underline-offset: .16em; text-decoration-thickness: 1px; }
a:hover { color: var(--pink-hover); }
a:focus-visible { outline: 2px solid var(--yellow); outline-offset: 3px; border-radius: 2px; }
.site-header a, .site-footer a, .card { text-decoration: none; }

/* bold is weight and brightness, not colour - green speckled every page,
   but plain 600 on near-white body text didn't stand out either */
strong, b { font-weight: 700; color: var(--head); }

img { display: block; max-width: 100%; height: auto; margin: 1rem auto; border-radius: var(--radius); }
hr { border: 0; border-top: 1px solid var(--line); margin: 2.5rem 0; }

/* header
   Two images, no classes to target - so they're addressed by position:
   first paragraph = the wide blockout banner, second = the arkode logo,
   third = the nav. The dashboards write the same three paragraphs. */
/* The banner and logo ride up out of view and the nav lands against the top
   of the window. Sticky is clipped by its own parent, so the whole header is
   the sticky element and its top offset is negative by exactly the height of
   the part that's allowed to leave. */
.site-header {
  background: var(--bg-alt); text-align: center; border-bottom: 1px solid var(--green);
  position: sticky; top: calc(-1 * var(--chrome-up)); z-index: 50;
}
.site-header p:nth-child(3) { background: var(--bg-alt); }
@media (max-width: 640px) { :root { --chrome-up: 130px; --nav-h: 53px; } }

/* BREADCRUMB - every level above this page, each one a link. It sits outside
   the content column and parks under the nav on scroll, so it stays reachable
   without taking a line out of the writing. Root pages emit none at all. */
.crumbs {
  position: sticky; top: var(--nav-h); z-index: 45;
  background: var(--bg-alt); border-bottom: 1px solid var(--line);
  padding: .45rem 0; font-size: .8rem;
}
.crumbs-inner {
  display: block; width: min(var(--measure), 100% - var(--gutter));
  margin: 0 auto;
  white-space: nowrap; overflow-x: auto; scrollbar-width: none;
}
.crumbs a { color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--green); }
.crumb-sep { color: var(--line); padding: 0 .45rem; }

/* ON-PAGE CONTENTS - the sections of THIS page, in the left margin.
   Built by Pandoc's --toc from the headings, so it needs no maintenance and
   no note ever mentions it. The page is a centred column, so past 1180px
   there is dead space either side doing nothing; below that it isn't drawn
   at all, because a phone has no margin to put it in.

   Two things it does with :has() and structure rather than markup:
   the page's own h1 is the outer list item, and its link is hidden so the
   list starts at the sections - and a contents list with only ONE section in
   it is noise, so it removes itself. */
.page-toc { display: none; }
@media (min-width: 1180px) {
  .page-toc {
    display: block; position: fixed; left: 2rem; z-index: 40;
    /* Centred in the space BELOW the header, not in the window. Centring on
       the window put the top of a long list at 15vh, which is behind the
       header while the page is scrolled to the top - so the first few
       entries were cut off. top + bottom + margin-block:auto centres it
       inside the safe band instead, so it can never ride up under the nav
       and it still centres on shorter lists. */
    top: calc(var(--chrome-up) + var(--nav-h) + 1rem);
    bottom: 2rem;
    height: fit-content; margin-block: auto;
    max-height: calc(100vh - var(--chrome-up) - var(--nav-h) - 3rem);
    width: 12rem; overflow-y: auto;
    font-size: .78rem; line-height: 1.45;
    padding-left: .9rem; border-left: 1px solid var(--line);
  }
  .page-toc ul { list-style: none; margin: 0; padding: 0; }
  .page-toc > ul > li > a { display: none; }        /* that's the page title */
  .page-toc:not(:has(li + li)) { display: none; }   /* one section isn't a contents list */
  .page-toc li { margin: 0; }
  .page-toc a {
    display: block; padding: .3rem 0; color: var(--muted);
    text-decoration: none; border-left: 2px solid transparent; padding-left: .6rem;
    margin-left: -.6rem;
  }
  .page-toc a:hover { color: var(--text); border-left-color: var(--green); }
  .page-toc ul ul ul a { padding-left: 1.5rem; font-size: .74rem; }
}

@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }
.site-header p { margin: 0; }
.site-header p:first-child img {          /* banner: a strip, not a wall */
  width: 100%; max-width: none; height: 150px; object-fit: cover;
  margin: 0; border-radius: 0;
}
.site-header p:nth-child(2) img {         /* logo: sized, and lifted to overlap */
  width: 96px; max-width: none; height: auto;
  margin: -48px auto .1rem; position: relative;
}
@media (max-width: 640px) {
  .site-header p:first-child img { height: 96px; }
  .site-header p:nth-child(2) img { width: 68px; margin-top: -34px; }
}
.site-header a {
  display: inline-block; margin: 0 .5rem; padding: .9rem .2rem; color: var(--text);
  font-family: var(--font-head);
  text-transform: uppercase; letter-spacing: .05em; font-size: .85rem;
  border-bottom: 2px solid transparent;
}
.site-header a:hover { color: var(--green); }
/* the page you are on - the build marks the nav link of this page's root */
.site-header a.here { color: var(--head); border-bottom-color: var(--green); }

/* content
   The page runs full width and the SECTIONS carry the colour: every other one
   sits on --band, so the page reads as a stack of blocks rather than one long
   slab. --section-divs in the build is what makes this possible - CSS has no
   way to say "everything between this h2 and the next".

   Nothing is centred by a wrapper. Each piece of content sizes itself to
   min(measure, window minus gutters) and centres itself, so a band can run the
   full width of the screen while the text inside it stays a readable measure. */
.content { max-width: none; margin: 0; padding: 2rem 0 1rem; flex: 1 0 auto; }
.content.wide { --measure: 72rem; }         /* a dashboard, not a read */
.content section { max-width: none; }

:is(.content, .content section) > *:not(section):not(img) {
  width: min(var(--measure), 100% - var(--gutter));
  margin-left: auto; margin-right: auto;
}

/* A section is banded only if it owns an h2 - an h3 subsection stays inside
   the band it sits in rather than starting a new one.

   ODD, not even: the page title and its opening paragraphs are a block in
   their own right, sitting on the page colour, so the FIRST h2 section has to
   be the banded one or the two run together as one long slab. */
.content section:has(> h2) { padding: 3rem 0; }
.content section:has(> h2):nth-of-type(odd) { background: var(--band); }

/* Cards sit on the measure like everything else. They used to break out to
   68rem, which made the portfolio grid the only block on the site that
   didn't share a left edge with its own heading. The width won back is not
   worth the page looking like two documents. */
.content .cards, .content .cards.rows { width: min(var(--measure), 100% - var(--gutter)); }

/* paragraph gaps have to beat the line gaps inside them, or paragraphs
   don't separate: line-height 1.7 puts lines ~1.7em apart, so 1em margins
   made the block one grey slab */
p { margin: 0 0 1.4em; text-wrap: pretty; }

/* a paragraph that introduces a list keeps its list close, so "Gunzilla
   Games - Junior Level Designer" and its bullets read as one block */
.content p:has(+ ul), .content p:has(+ ol) { margin-bottom: .5em; }
ul, ol { margin: 0 0 1.4em; padding-left: 2.2rem; }
li { margin: .3rem 0; }
li::marker { color: var(--muted); }

/* one colour for headings - size, weight and space do the ranking.
   scroll-margin keeps an anchor jump off the top edge. */
.content h1, .content h2, .content h3, .content h4 {
  font-family: var(--font-head); color: var(--head);
  scroll-margin-top: 5rem;            /* clears the sticky nav */
  text-wrap: balance;
}
.content section[id] { scroll-margin-top: 5rem; }   /* --section-divs moves the anchor id onto the section */
.content h1 { font-size: 2.1rem; line-height: 1.25; margin: .2rem 0 1.4rem; }
/* no top margin: the band's own padding is the space above a section heading,
   and stacking the two is what made the gaps look uneven */
.content h2 { font-size: 1.5rem; margin: 0 0 1.4rem; padding-bottom: .5rem; border-bottom: 1px solid var(--line); }
.content h3 { font-size: 1.2rem; margin: 2.2rem 0 .8rem; }
.content h4 {                                 /* a label, not a third shout */
  font-size: .78rem; margin: 1.6rem 0 .5rem;
  color: var(--muted); text-transform: uppercase; letter-spacing: .09em; font-weight: 600;
}

/* images sit inside the column instead of defining it, and the space below
   is bigger than the space above so a picture belongs to what it follows */
.content img { max-width: min(560px, 100% - var(--gutter)); margin: 1.25rem auto 2rem; }

/* IMAGE CLUSTERS, with no new markdown syntax: put two or more images in the
   SAME paragraph (consecutive lines, no blank line between) and Pandoc emits
   them in one <p>. :has() spots that and lays them out in a row.

   TWO to a row, never more. Fitting three across made every one of them too
   small to read and pushed the block far wider than the text it belongs to;
   two means each image is big enough to actually look at. A set with an odd
   number lets its LAST image run the full width rather than leaving a
   half-empty row, which is what makes three or five sit properly.
   Below 52em they stack one to a row. */
.content p:has(img + img) {
  display: grid; grid-template-columns: 1fr 1fr; gap: .6rem;
  width: min(52rem, 100% - var(--gutter));
  margin: 1.5rem auto 2.25rem;
}
.content p:has(img + img) img {
  width: 100%; height: auto; max-width: none; min-width: 0;
  aspect-ratio: 16 / 10; object-fit: cover;
  margin: 0;
}
.content p:has(img + img) img:last-child:nth-child(odd) {
  grid-column: 1 / -1; aspect-ratio: 21 / 9;
}
@media (max-width: 52em) {
  .content p:has(img + img) { grid-template-columns: 1fr; }
  .content p:has(img + img) img:last-child:nth-child(odd) { aspect-ratio: 16 / 10; }
}

/* A single image sharing a paragraph with words sits beside them. Only where
   there's room: on a phone it drops back to a full-width block, because a
   third of a phone screen is not a picture. */
@media (min-width: 46em) {
  .content p.with-text:not(:has(img + img)) > img {
    float: right; width: 42%; max-width: 17rem;
    margin: .3rem 0 1rem 1.75rem;
  }
  .content p.with-text:not(:has(img + img)) { overflow: visible; }
}

/* The line under a page title, printed from frontmatter (or by the script
   that drew the page) rather than typed into the writing - so the date shown
   and the date sorted by can't drift apart. Pulled up under the h1's own
   bottom margin so it reads as part of the title block. */
.content .page-meta {
  margin-top: -1.1rem; margin-bottom: 2.2rem;
  color: var(--muted); font-size: .85rem;
}

/* A LINE OF LINKS BECOMES A ROW OF BUTTONS, with no new markdown syntax:
   write "[Portfolio »](/portfolio) · [Qualifications ↓](#qualifications)" as
   a paragraph of its own and the build marks it, because a paragraph that is
   nothing but links is never prose - it's a set of choices.

   Outlined, not filled. The brand pink is a background colour that white
   text only just clears, and a row of solid pink slabs would be the loudest
   thing on any page it sat on.

   ONE rule dresses every button on the site and every switch label on the
   dashboards. The chosen label of a switch is the same button, lit. */
p.button-row {
  display: flex; flex-wrap: wrap; gap: .6rem;
  margin: 1.7rem auto 2.1rem;
}
p.button-row a, .switch label {
  display: inline-block; padding: .55rem 1.2rem;
  font-family: var(--font-head); font-size: .85rem; letter-spacing: .03em;
  color: var(--pink-text); background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--radius);
  text-decoration: none; cursor: pointer;
  transition: color .2s ease, border-color .2s ease, background .2s ease, transform .2s ease;
}
p.button-row a:hover, .switch label:hover {
  color: var(--pink-hover); border-color: var(--pink);
  background: var(--bg-alt);
}
p.button-row a:hover { transform: translateY(-2px); }

/* THE FEED BUTTON, on the articles page. The row it sits in is the ordinary
   button-row above; all this adds is the universal RSS glyph, so the button
   is recognisable at a glance without a word of explanation.

   The icon is a MASK rather than a coloured image, so it takes the link's own
   colour and moves with it on hover. A fixed orange square would be the one
   thing on the page not speaking the palette, and this site has never shouted
   to be noticed. */
p.button-row a.rss::before {
  --glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='6.18' cy='17.82' r='2.18'/%3E%3Cpath d='M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z'/%3E%3C/svg%3E");
  content: "";
  display: inline-block;
  width: .8em; height: .8em;
  margin-right: .5em;
  vertical-align: -.02em;
  background: currentColor;
  -webkit-mask: var(--glyph) center / contain no-repeat;
  mask: var(--glyph) center / contain no-repeat;
}

/* THE SWITCH - a row of buttons choosing which of several panes is on show,
   or which cards stay in a grid. Pure CSS: the radios hold the state and
   :has() reads it. No script, and no CSS is generated per switch either: the
   rule is POSITIONAL - the nth option shows the nth pane - so the same ten
   rules serve a two-way CV switch, a four-way chart range and a seven-topic
   fact filter alike. A switch may have up to ten options.

   Markup (the build and the dashboard scripts both write exactly this):

     <div class="sw">
       <div class="switch">
         <input class="pick" type="radio" name="x" id="x-1" checked><label for="x-1">A</label>
         <input class="pick" type="radio" name="x" id="x-2"><label for="x-2">B</label>
       </div>
       <div class="panes">
         <div>pane A</div>
         <div>pane B</div>
       </div>
     </div>

   A first option marked class="pick all" shows every pane (a filter's "All").
   A switch that sits under the page title (class="switch page", a direct
   child of main) drives every div.panes.page anywhere below it, so one range
   picker can run several charts down the page.

   A card grid filters instead of paning: <div class="sw tags"> holds the
   switch and the .cards; each card carries k2, k3... for the options it
   answers to, and the nth option hides every card without k<n>.

   The radios are moved out of sight rather than display:none'd, so they stay
   focusable and the group still behaves as keyboard radios; the chosen state
   and the focus ring are drawn on the LABEL, because the input is off-screen. */
.pick {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.switch {
  width: min(var(--measure), 100% - var(--gutter));
  margin: 1.7rem auto -0.5rem;
  display: flex; flex-wrap: wrap; gap: .6rem;
}
.content > .switch.page { margin: -1rem auto 1.4rem; }
.pick:checked + label { color: var(--head); border-color: var(--green); background: var(--bg-alt); }
.pick:focus-visible + label { outline: 2px solid var(--yellow); outline-offset: 3px; }
.switch label i { font-style: normal; opacity: .55; margin-left: .35em; }   /* a count after the label */

.sw:has(> .switch > .pick:nth-of-type(1):not(.all):checked) > .panes > :not(:nth-child(1)),
.sw:has(> .switch > .pick:nth-of-type(2):checked) > .panes > :not(:nth-child(2)),
.sw:has(> .switch > .pick:nth-of-type(3):checked) > .panes > :not(:nth-child(3)),
.sw:has(> .switch > .pick:nth-of-type(4):checked) > .panes > :not(:nth-child(4)),
.sw:has(> .switch > .pick:nth-of-type(5):checked) > .panes > :not(:nth-child(5)),
.sw:has(> .switch > .pick:nth-of-type(6):checked) > .panes > :not(:nth-child(6)),
.sw:has(> .switch > .pick:nth-of-type(7):checked) > .panes > :not(:nth-child(7)),
.sw:has(> .switch > .pick:nth-of-type(8):checked) > .panes > :not(:nth-child(8)),
.sw:has(> .switch > .pick:nth-of-type(9):checked) > .panes > :not(:nth-child(9)),
.sw:has(> .switch > .pick:nth-of-type(10):checked) > .panes > :not(:nth-child(10)),
.content:has(> .switch.page > .pick:nth-of-type(1):not(.all):checked) .panes.page > :not(:nth-child(1)),
.content:has(> .switch.page > .pick:nth-of-type(2):checked) .panes.page > :not(:nth-child(2)),
.content:has(> .switch.page > .pick:nth-of-type(3):checked) .panes.page > :not(:nth-child(3)),
.content:has(> .switch.page > .pick:nth-of-type(4):checked) .panes.page > :not(:nth-child(4)),
.content:has(> .switch.page > .pick:nth-of-type(5):checked) .panes.page > :not(:nth-child(5)),
.content:has(> .switch.page > .pick:nth-of-type(6):checked) .panes.page > :not(:nth-child(6)),
.content:has(> .switch.page > .pick:nth-of-type(7):checked) .panes.page > :not(:nth-child(7)),
.content:has(> .switch.page > .pick:nth-of-type(8):checked) .panes.page > :not(:nth-child(8)),
.content:has(> .switch.page > .pick:nth-of-type(9):checked) .panes.page > :not(:nth-child(9)),
.content:has(> .switch.page > .pick:nth-of-type(10):checked) .panes.page > :not(:nth-child(10)),
.sw.tags:has(> .switch > .pick:nth-of-type(2):checked) > .cards > .card:not(.k2),
.sw.tags:has(> .switch > .pick:nth-of-type(3):checked) > .cards > .card:not(.k3),
.sw.tags:has(> .switch > .pick:nth-of-type(4):checked) > .cards > .card:not(.k4),
.sw.tags:has(> .switch > .pick:nth-of-type(5):checked) > .cards > .card:not(.k5),
.sw.tags:has(> .switch > .pick:nth-of-type(6):checked) > .cards > .card:not(.k6),
.sw.tags:has(> .switch > .pick:nth-of-type(7):checked) > .cards > .card:not(.k7),
.sw.tags:has(> .switch > .pick:nth-of-type(8):checked) > .cards > .card:not(.k8),
.sw.tags:has(> .switch > .pick:nth-of-type(9):checked) > .cards > .card:not(.k9),
.sw.tags:has(> .switch > .pick:nth-of-type(10):checked) > .cards > .card:not(.k10) { display: none; }
/* the panes and the tag grid sit on the measure like any other block; the
   .sw wrapper itself is transparent to layout */
.content .sw, .content section > .sw { width: auto; }
.content .sw > .panes > *, .content .sw > .switch, .content .sw.tags > .cards {
  width: min(var(--measure), 100% - var(--gutter)); margin-left: auto; margin-right: auto;
}
.sw.tags > .cards { margin-top: 1.2rem; }

/* THE CHIP - a small pill for a tag, a state, a count. The site's card tags
   and the dashboards' chips are the same thing. */
.chip, .card-tags b {
  display: inline-block; font-size: .72rem; font-weight: normal;
  color: var(--muted); background: var(--bg);
  border: 1px solid var(--line); border-radius: 999px;
  padding: .05rem .55rem; white-space: nowrap;
  transition: border-color .25s ease, color .25s ease;
}

blockquote {
  margin: 1.2rem auto; padding: .45rem 1rem;
  border-left: 3px solid var(--pink); background: var(--panel);
  color: var(--text); font-style: italic;
  font-size: .92rem; line-height: 1.55;
}
blockquote p { margin: 0 0 .6em; }
blockquote p:last-child { margin-bottom: 0; }

/* an embedded PDF, built from an ![[file.pdf]] embed in the markdown. Two of
   them side by side in a section become a switch - see the build. */
.pdf-embed { width: min(var(--measure), 100% - var(--gutter)); margin: 1.5rem auto 2rem; }
.pdf-embed iframe {
  display: block; width: 100%; aspect-ratio: 1 / 1.35;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel);
  margin: 0;
}

code { background: var(--bg); padding: .1rem .35rem; border-radius: 4px; font-family: var(--font-mono); font-size: .88em; }
pre { background: var(--bg); padding: 1rem; border-radius: var(--radius); overflow-x: auto; border: 1px solid var(--line); }
pre code { background: none; padding: 0; }

/* tables span the measure. The old problem - a one-word grade flung 800px
   from its row - is solved by the narrower column, not by shrinking them.

   A table is reference material, not reading text, so it is allowed more room
   than the 44rem measure: four columns of prose at 44rem is a column of
   single words. It still centres on the same axis as everything else, and
   the box never exceeds the window. Anything wider than the box scrolls
   INSIDE it, so a wide table can no longer widen the whole page.
   The build strips Pandoc's inline width and adds this wrapper. */
.content .table-scroll {
  width: min(64rem, 100% - var(--gutter));
  margin: 1.5rem auto; overflow-x: auto;
}
.content .table-scroll > table { margin: 0; min-width: 100%; }
table { border-collapse: collapse; margin: 1.5rem auto; width: 100%; }
th, td { border: 1px solid var(--line); padding: .55rem .9rem; text-align: left; }
th { background: var(--panel); color: var(--head); font-weight: 600; }
tbody tr:nth-child(odd) { background: var(--band); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
.pos { color: var(--green); }
.neg { color: var(--rose-text); }

/* ---------------------------------------------------------------------------
   THE REST OF THE MARKDOWN STANDARD

   Everything above styles what the pages happened to use. This styles the rest
   of what Pandoc can emit, so reaching for a definition list or a footnote
   lands designed instead of on browser defaults - which is what stopped the
   Mosaic page using either. Audited 2026-08-06 by rendering a document that
   exercises every construct and diffing the tags and classes it produced
   against the selectors in this file.
   --------------------------------------------------------------------------- */

/* Definition lists. The term carries the weight and the definition is set off
   a rule, so a long run reads as pairs rather than one column of text. This is
   the shape to reach for when a bulleted list is really term-and-explanation:
   the term becomes scannable and the prose stops competing with it. */
dl { margin: 1.4rem auto; }
dt { font-family: var(--font-head); color: var(--head); font-weight: 600; margin: 1.3rem 0 .4rem; }
dt:first-child { margin-top: 0; }
dd { margin: 0 0 1rem; padding-left: 1rem; border-left: 1px solid var(--line); }
dd > :last-child { margin-bottom: 0; }

/* Footnotes. The marker stays small enough not to interrupt the line it sits
   in, and the notes are set back so the end of an article doesn't read as more
   article. Pandoc's own <hr> above them is already styled and already sized to
   the measure, so it does the dividing. */
.footnote-ref { text-decoration: none; font-size: .8em; padding: 0 .15em; }
.footnotes { margin-top: 2.5rem; color: var(--muted); font-size: .92rem; }
.footnotes ol { margin: .8rem auto; }
.footnotes li { margin: .5rem 0; }
.footnote-back { text-decoration: none; margin-left: .35em; }

/* Fifth and sixth level headings. Rare, but without these a deep page falls
   back to browser defaults partway down. */
.content h5, .content h6 {
  font-family: var(--font-head); scroll-margin-top: 5rem;
  font-size: .95rem; font-weight: 600; margin: 1.5rem 0 .4rem;
}
.content h5 { color: var(--text); }
.content h6 { color: var(--muted); }

/* A table's caption, which Pandoc places above the table it describes. */
caption { caption-side: top; text-align: left; color: var(--muted); font-size: .9rem; padding-bottom: .5rem; }

/* A figure is an image that came with a caption. */
figure { margin: 1.25rem auto 2rem; }
.content figure img { margin: 0 auto .6rem; }
figcaption { color: var(--muted); font-size: .9rem; text-align: center; }

/* Task lists drop the bullet - the box is the marker. */
ul.task-list { list-style: none; padding-left: .3rem; }
ul.task-list li input { margin-right: .5em; accent-color: var(--pink); }

/* A line block keeps the breaks the author typed: addresses, verse, anything
   where the wrapping is the meaning. */
.line-block { margin: 1.4rem auto; line-height: 1.6; }

/* Inline marks. Some of these only arrive through raw HTML in the markdown,
   which is itself part of the standard. */
sub, sup { font-size: .75em; line-height: 0; }
del { color: var(--muted); }
ins { text-decoration: none; border-bottom: 1px solid var(--green); }
mark { background: var(--pink); color: var(--head); padding: 0 .2em; border-radius: 3px; }
abbr[title] { text-decoration: underline dotted; cursor: help; }
kbd {
  background: var(--panel); border: 1px solid var(--line); border-bottom-width: 2px;
  border-radius: 4px; padding: .05rem .35rem;
  font-family: var(--font-mono); font-size: .85em;
}

iframe, video { display: block; width: 100%; aspect-ratio: 16/9; border: 0; margin: 1.2rem auto; border-radius: var(--radius); background: var(--bg); }

/* A block-level replaced element is not allowed to overlap a float, so it
   gets shunted sideways into whatever space is left rather than sitting on
   the measure like everything else. On Off the Grid the floated logo hangs
   below its own paragraph and pushed the video 420px left of every other
   block on the page. `clear` is the fix: a video belongs under the text it
   follows, never beside a logo. Same trap waits for tables and image grids,
   which establish their own formatting context for the same reason. */
.content iframe, .content video, .content table, .content p:has(img + img) { clear: both; }

/* portfolio cards - no JavaScript anywhere; the movement is all CSS.
   Three across, fixed rather than auto-filled: auto-fill sizes itself to a
   minimum card width, which is what let the grid decide the column's width
   instead of the other way round. Three is the count that fills the measure
   without leaving a half-empty row on a six-piece portfolio. */
.cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
  margin: 1.2rem 0 2.6rem;
}
/* two across before the cards get too narrow to read, then one. Card ROWS
   are a single column at every size and set their own rules below. */
@media (max-width: 46em) { .cards:not(.rows) { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 30em) { .cards:not(.rows) { grid-template-columns: 1fr; } }
.card {
  display: flex; flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line); border-radius: 8px;
  overflow: hidden;
  color: var(--text);
  transition: transform .25s ease, border-color .25s ease, box-shadow .25s ease;
  animation: card-in .45s ease-out both;
}
.card:hover, .card:focus-visible {
  transform: translateY(-6px);
  box-shadow: 0 10px 26px rgba(0,0,0,.45);
}
/* the three brand colours cycle across the grid, so no two neighbours match */
.card:nth-child(3n+1):hover, .card:nth-child(3n+1):focus-visible { border-color: var(--pink); }
.card:nth-child(3n+2):hover, .card:nth-child(3n+2):focus-visible { border-color: var(--green); }
.card:nth-child(3n+3):hover, .card:nth-child(3n+3):focus-visible { border-color: var(--yellow); }

.card-img { display: block; overflow: hidden; aspect-ratio: 16 / 10; background: var(--bg); }
.card-img img {
  width: 100%; height: 100%; max-width: none; object-fit: cover;
  margin: 0; border-radius: 0;
  transition: transform .5s ease;
}
.card:hover .card-img img, .card:focus-visible .card-img img { transform: scale(1.07); }

.card-body { display: block; padding: .85rem 1rem 1rem; }
.card-title { display: block; font-family: var(--font-head); color: var(--head); font-size: 1.05rem; line-height: 1.35; }
.card-role {
  display: block; font-family: var(--font-head); color: var(--green);
  font-size: .74rem; text-transform: uppercase; letter-spacing: .07em; margin-top: .2rem;
}
/* only ever a publication date - `updated` is housekeeping and never shown */
.card-date {
  display: block; font-family: var(--font-head); color: var(--muted);
  font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; margin-top: .3rem;
}
.card-desc {                                  /* clamped so cards stay level */
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
  color: var(--muted); font-size: .85rem; line-height: 1.5; margin-top: .55rem;
}
.card-tags { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .75rem; }
.card-tags b { font-size: .7rem; }
.card:hover .card-tags b { border-color: var(--muted); color: var(--text); }

/* Article rows - the same card laid on its side. One column, image left,
   text right, and it collapses back to a stacked card on a narrow screen. */
.cards.rows { grid-template-columns: 1fr; gap: .9rem; }
.cards.rows .card { flex-direction: row; align-items: stretch; }
.cards.rows .card-img { flex: 0 0 30%; max-width: 260px; aspect-ratio: 16 / 9; }
.cards.rows .card-body { flex: 1 1 auto; padding: 1rem 1.2rem; }
.cards.rows .card-desc { -webkit-line-clamp: 2; }
.cards.rows .card:hover, .cards.rows .card:focus-visible { transform: translateX(6px); }

@media (max-width: 620px) {
  .cards.rows .card { flex-direction: column; }
  .cards.rows .card-img { flex-basis: auto; max-width: none; }
}

/* cards arrive in sequence rather than all at once */
@keyframes card-in { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
.card:nth-child(1) { animation-delay: 0s }
.card:nth-child(2) { animation-delay: .07s }
.card:nth-child(3) { animation-delay: .14s }
.card:nth-child(4) { animation-delay: .21s }
.card:nth-child(5) { animation-delay: .28s }
.card:nth-child(6) { animation-delay: .35s }
.card:nth-child(7) { animation-delay: .42s }
.card:nth-child(8) { animation-delay: .49s }

@media (prefers-reduced-motion: reduce) {
  .card, .card-img img, .card-tags b, p.button-row a, .switch label { animation: none; transition: none; }
  p.button-row a:hover { transform: none; }
}

/* printing - the CV page is the one people actually print */
@media print {
  .site-header, .site-footer, .pdf-embed, .switch, .crumbs, .page-toc { display: none; }
  body { background: #fff; color: #000; font-size: 11pt; }
  .content section:has(> h2) { padding: .5rem 0; background: none !important; }
  .content h1, .content h2, .content h3, .content h4,
  .content h5, .content h6, dt, strong, b { color: #000; }
  dd { border-left-color: #999; }
  mark { background: none; border-bottom: 2px solid #000; color: #000; }
  a { color: #000; text-decoration: underline; }
  .content section { break-inside: auto; }
  h2, h3 { break-after: avoid; }
}

/* footer */
.site-footer { background: var(--bg-alt); text-align: center; padding: 1.5rem 1rem; border-top: 1px solid var(--green); color: var(--muted); font-size: .9rem; margin-top: auto; }
.site-footer p { margin: .25rem 0; }
.site-footer a { color: var(--muted); margin: 0 .4rem; }
.site-footer a:hover { color: var(--green); }
.site-footer code { font-size: .8em; }

/* THE WAY BACK UP - the foot of every page that has a parent. The breadcrumb
   does the same job at the top; this one is for a reader who has just finished.
   Same voice as the crumbs: quiet until hovered. */
.content p.up {
  width: min(var(--measure), 100% - var(--gutter));
  margin: 2.8rem auto 0; font-size: .85rem;
}
.content p.up a { color: var(--muted); text-decoration: none; }
.content p.up a:hover { color: var(--green); }


/* ===========================================================================
   THE WEBSITE'S OWN LOOK. Everything above is shared with the dashboards and
   the CV; this block is what only arkodegames.com does, so each rule that
   changes page structure is scoped to body.site (the website's template sets
   it, the dashboards never do). Decided from the twenty-suggestion demo on
   14/09; the numbers in the comments are that note's. Not adopted: cross-page
   view transitions (a white frame on some page changes), film grain, the old
   je-font. No script anywhere: motion is scroll timelines and transitions,
   state is CSS. Reduced motion is honoured at the end of the block.

   Vocabulary used throughout: one small mono voice for every readout on the
   site - section numbers, the spec strip's labels, a card's stage badge, an
   image caption - so they read as one instrument panel.
   ======================================================================== */

.readout, .site .content h2::before, .site .content h3::before,
.page-spec, .card[data-stage]::before, .site figcaption {
  font-family: var(--font-mono); letter-spacing: .1em;
}

/* 2. SCROLL-DRIVEN REVEALS. Blocks and cards fade and rise as they enter the
   window, on the compositor. The range is a LENGTH so a heading and a tall
   grid take the same 220px of travel. Replaces the load-time card stagger on
   the site (the dashboards keep theirs). Chrome, Edge, Safari 26, Firefox 143+. */
@keyframes rise-in { from { opacity: 0; translate: 0 14px; } to { opacity: 1; translate: 0 0; } }
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .site .content section > :is(h2, h3, p, ul, ol, figure, blockquote, iframe, details, .table-scroll, .pdf-embed):not(.page-meta, .page-spec, .up),
    .site .content .card {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 220px;
    }
  }
}

/* 3. READING PROGRESS, with the page's sections marked on it. The 2px line
   under the nav is the scroll position. The markers are the root scroller's
   own scroll markers, one per h2 section: each a link to its section, the one
   on screen lit yellow (the third brand colour, kept for this and the focus
   ring). They sit at the right end of the line and ride up with the header
   until it sticks. CSS cannot place them where the sections fall on the page
   (that needs layout, i.e. script), so they are evenly spaced: a section
   index, not a ruler. Hidden on a phone, where the nav fills the strip. */
@keyframes grow-x { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes ride-up { from { translate: 0 0; } to { translate: 0 calc(-1 * var(--chrome-up)); } }
@supports (animation-timeline: scroll()) {
  .site .site-header::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;
    background: var(--pink); transform-origin: 0 50%; z-index: 2;
    animation: grow-x linear both; animation-timeline: scroll(root);
  }
}
@supports (scroll-marker-group: after) and (animation-timeline: scroll()) {
  html:has(> body.site) { scroll-marker-group: after; }
  html:has(> body.site)::scroll-marker-group {
    position: fixed; right: 1rem; z-index: 52;
    top: calc(var(--chrome-up) + var(--nav-h) - 5px);
    display: flex; gap: .45rem; padding: 0;
    animation: ride-up linear both; animation-timeline: scroll(root); animation-range: 0 var(--chrome-up);
  }
  .site .content section:has(> h2)::scroll-marker {
    content: ""; width: 8px; height: 8px; border-radius: 2px;
    background: var(--bg-alt); border: 1px solid var(--muted); cursor: pointer;
  }
  .site .content section:has(> h2)::scroll-marker:hover { border-color: var(--yellow); }
  .site .content section:has(> h2)::scroll-marker:target-current { background: var(--yellow); border-color: var(--yellow); }
  .site .content section:has(> h2)::scroll-marker:focus-visible { outline: 2px solid var(--yellow); outline-offset: 2px; }
  @media (max-width: 640px) { html:has(> body.site)::scroll-marker-group { display: none; } }
}

/* 4. THE NAV KNOWS WHEN IT IS STUCK: a shadow and brighter links once the
   banner has scrolled away. Chrome and Edge 133+; elsewhere the nav is as is. */
.site .site-header { container-type: scroll-state; }
@container scroll-state(stuck: top) {
  .site-header p:nth-child(3) { box-shadow: 0 10px 24px rgba(0,0,0,.45); background: var(--panel); }
  .site-header p:nth-child(3) a { color: var(--head); }
}

/* 5. THE CONTENTS LIST MARKS THE SECTION IN VIEW. Chrome, Edge; elsewhere unchanged. */
.page-toc { scroll-target-group: auto; }
.page-toc a:target-current { color: var(--head); border-left-color: var(--green); }

/* 6. BANNER TINT. A pink-to-green wash ties the blockout render to the palette. */
.site .site-header p:first-child { position: relative; overflow: hidden; }
.site .site-header p:first-child::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(105deg, var(--pink), var(--green));
  mix-blend-mode: overlay; opacity: .38;
}

/* 7. BLUEPRINT PAPER. The page itself is dotted at the crossings of a 24px
   grid (the title block, the plain sections and the foot all show it, as one
   continuous sheet); the banded sections paint over it with the same grid as
   faint lines. Two rulings of one sheet, both under 5% contrast. */
.site .content {
  background-image: radial-gradient(rgba(255,255,255,.055) 1px, transparent 1.5px);
  background-size: 24px 24px; background-position: 12px 12px;
}
.site .content section:has(> h2):nth-of-type(odd) {
  background-color: var(--band);
  background-image:
    linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px);
  background-size: 24px 24px;
}

/* 9. SECTION MARKERS. 01, 02 before each h2 and 02.1 before each h3 inside
   it, in green. CSS counters, no markup. An h3 straight under the title has
   no sector to belong to and stays unnumbered. */
.site .content { counter-reset: sec; }
.site .content section:has(> h2) { counter-increment: sec; counter-reset: sub; }
.site .content section:has(> h3) { counter-increment: sub; }
.site .content h2::before, .site .content h3::before {
  font-size: .7em; font-weight: 400; color: var(--green); margin-right: .7em; vertical-align: .14em;
}
.site .content h2::before { content: counter(sec, decimal-leading-zero); }
.site .content section:has(> h2) h3::before { content: counter(sec, decimal-leading-zero) "." counter(sub); }

/* 10 + 11. ONE HOVER LANGUAGE FOR IMAGES. Every content image answers a
   hover the same way, at the same speed: it lifts a touch; in a cluster the
   others dim; a figure's caption (Pandoc makes one from alt text written
   alone on the image's line) rides up over the corner as a label, and is
   always shown on touch. One transition per element, so nothing fights. */
.site :is(.content, .content section) > figure:not(section):not(img) {
  width: fit-content; max-width: min(560px, 100% - var(--gutter)); position: relative;
}
.site .content figure img { margin: 0; max-width: 100%; }
.site .content :is(figure, p:has(img + img)) img { transition: transform .3s ease, filter .3s ease; }
.site .content figure:hover img, .site .content p:has(img + img) img:hover { transform: scale(1.02); }
.site .content p:has(img + img):hover img:not(:hover) { filter: brightness(.55); }
.site figcaption {
  position: absolute; left: .7rem; bottom: .7rem; margin: 0;
  font-size: .68rem; text-transform: uppercase; text-align: left;
  color: var(--head); background: color-mix(in srgb, var(--bg) 85%, transparent);
  border-left: 2px solid var(--green); padding: .3rem .6rem;
  opacity: 0; translate: 0 4px; transition: opacity .3s ease, translate .3s ease;
}
.site figure:hover figcaption, .site figure:focus-within figcaption { opacity: 1; translate: 0 0; }
@media (hover: none) { .site figcaption { opacity: 1; translate: 0 0; } }

/* 12. FILMSTRIP. A cluster of three or more images becomes a snap-scrolling
   strip with native dots and arrows, so a gallery stops being a wall.
   Chrome, Edge 135+; elsewhere the two-column grid above stands. */
@supports (scroll-marker-group: after) {
  .site .content p:has(img + img + img) {
    display: flex; gap: .6rem;
    overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none;
    scroll-marker-group: after;
    anchor-name: --strip; anchor-scope: --strip;   /* each strip's buttons find their own strip */
    margin-bottom: 0;   /* the dots below carry the gap to whatever follows */
  }
  .site .content p:has(img + img + img) img { flex: 0 0 100%; scroll-snap-align: center; aspect-ratio: 16 / 9; }
  .site .content p:has(img + img + img) img:last-child:nth-child(odd) { aspect-ratio: 16 / 9; }   /* outranks the grid's 21:9 odd-last rule */
  .site .content p:has(img + img + img)::scroll-marker-group { display: flex; justify-content: center; gap: .45rem; padding: .8rem 0 2.25rem; }
  .site .content p:has(img + img + img) img::scroll-marker {
    content: ""; width: 9px; height: 9px; border-radius: 50%;
    background: var(--line); border: 1px solid var(--muted);
  }
  .site .content p:has(img + img + img) img::scroll-marker:target-current { background: var(--pink); border-color: var(--pink); }
  .site .content p:has(img + img + img)::scroll-button(*) {
    position: absolute; position-anchor: --strip; top: anchor(center); translate: 0 -50%;
    width: 2.4rem; height: 2.4rem; border-radius: 50%; border: 1px solid var(--line);
    background: color-mix(in srgb, var(--bg) 80%, transparent); color: var(--head);
    font-size: 1.5rem; line-height: 1; cursor: pointer; z-index: 2;
  }
  .site .content p:has(img + img + img)::scroll-button(left)  { content: "\2039" / "Previous image"; left: anchor(left); margin-left: .5rem; }
  .site .content p:has(img + img + img)::scroll-button(right) { content: "\203A" / "Next image"; right: anchor(right); margin-right: .5rem; }
  .site .content p:has(img + img + img)::scroll-button(*):disabled { opacity: .25; cursor: default; }
}

/* 14. NUMBERS THAT LINE UP: every digit the same width, so dates on
   neighbouring cards and columns in tables align; the mono zero slashed. */
.page-meta, .card-date, .page-spec, .site table { font-variant-numeric: tabular-nums slashed-zero; }

/* 15. THE UNDERLINE GROWS on hover, every prose link. */
.site a { transition: color .15s ease, text-decoration-thickness .15s ease; }
.site a:hover { text-decoration-thickness: 2px; }

/* 16. COLLAPSIBLE SECTIONS. The build writes <details> from an Obsidian
   folded callout - "> [!note]- Title" folds, "> [!note]+ Title" starts open,
   the body stays markdown (the rule lives beside Callouts in the build). The
   height animates to auto, no script. */
.content details {
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel);
  margin: 1.4rem auto; interpolate-size: allow-keywords;
}
.content summary {
  cursor: pointer; padding: .7rem 1rem; list-style: none;
  font-family: var(--font-head); color: var(--head);
  display: flex; align-items: center; gap: .6rem;
}
.content summary::-webkit-details-marker { display: none; }
.content summary::after { content: "+"; margin-left: auto; color: var(--green); font-size: 1.2rem; transition: rotate .2s ease; }
.content details[open] > summary::after { rotate: 45deg; }
.content details::details-content {
  block-size: 0; overflow-y: clip; padding: 0 1rem;
  transition: block-size .3s ease, content-visibility .3s allow-discrete, padding .3s ease;
}
.content details[open]::details-content { block-size: auto; padding-bottom: .4rem; }

/* 17. THE TITLE ARRIVES. On first paint the h1 and the lines under it fade
   up 10px over half a second, then hold still: one motion, once, at the top. */
@media (prefers-reduced-motion: no-preference) {
  .site .content h1, .site .content .page-meta, .site .content .page-spec {
    transition: opacity .5s ease-out, translate .5s ease-out;
    @starting-style { opacity: 0; translate: 0 10px; }
  }
}

/* 18. THE SPEC STRIP. Role, stage, tags and features from the frontmatter,
   written by the build as a loading-screen readout under the title (the date
   stays on the meta line above, once). Cells stack on a phone. */
.content .page-meta:has(+ .page-spec) { margin-bottom: 1rem; }
.page-spec {
  display: flex; flex-wrap: wrap; align-items: stretch;
  margin: 0 auto 2.2rem; padding: 0;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
}
.page-spec > div { display: flex; flex-direction: column; gap: .2rem; padding: .6rem 1.1rem .6rem 0; margin-right: 1.1rem; border-right: 1px solid var(--line); }
.page-spec > div:last-child { border-right: 0; margin-right: 0; }
.page-spec dt { font: inherit; font-size: .6rem; text-transform: uppercase; color: var(--muted); margin: 0; }
.page-spec dd { font-size: .8rem; color: var(--head); margin: 0; padding: 0; border: 0; letter-spacing: .02em; }
.page-spec .spec-stage dd { text-transform: capitalize; }
.page-spec .spec-features { flex-basis: 100%; border-right: 0; border-top: 1px solid var(--line); margin-right: 0; padding-right: 0; }
.page-spec .spec-features dd { display: flex; flex-wrap: wrap; gap: .3rem 1.1rem; font-size: .76rem; color: var(--text); }
.page-spec .spec-features dd span::before { content: "\25B8 "; color: var(--green); }
@media (max-width: 640px) {
  .page-spec > div { flex-basis: 100%; border-right: 0; margin-right: 0; padding-right: 0; border-bottom: 1px solid var(--line); }
  .page-spec > div:last-child { border-bottom: 0; }
  .page-spec .spec-features { border-top: 0; }
}

/* 19. STAGE BADGE. The build writes data-stage on a card from the page's
   `stage`; a corner tag says it at a glance: green shipped, pink in
   progress, muted for the rest. */
.card[data-stage] { position: relative; }
.card[data-stage]::before {
  content: attr(data-stage); position: absolute; top: .6rem; left: .6rem; z-index: 1;
  font-size: .62rem; text-transform: uppercase;
  padding: .18rem .5rem; border-radius: 3px;
  background: color-mix(in srgb, var(--bg) 85%, transparent);
  color: var(--green); border: 1px solid currentColor;
}
.card[data-stage="archived"]::before, .card[data-stage="concept"]::before { color: var(--muted); }
.card[data-stage="wip"]::before, .card[data-stage="active"]::before { color: var(--pink-text); }

/* 20. PREVIOUS AND NEXT at the foot of a page, among the pages its hub lists
   under the same heading, in the hub's order. The build writes an ordinary
   button-row, so the buttons need nothing new; this only pins them to the
   two ends. */
p.button-row.siblings { justify-content: space-between; margin: 2.6rem auto 0; }
.siblings a { max-width: 48%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.siblings a.next { margin-left: auto; }

/* the motion above honours the reader's setting */
@media (prefers-reduced-motion: reduce) {
  .content details::details-content, .site figcaption, .site a, .site .content :is(figure, p:has(img + img)) img { transition: none; }
}
@media print { .page-spec { border-color: #999; } .siblings { display: none; } }
