/* ============================================================
   hackenshaw.com — the house style (dark CRT terminal)

   ONE copy, linked by EVERY page on every vhost:
       /index.html                    hackenshaw.com
       /projects/index.html           projects.hackenshaw.com
       /presentations/index.html      presentations.hackenshaw.com
       /projects/cmdbox/index.html    cmdbox.hackenshaw.com
       /opt/animated-qr/web/          qr.hackenshaw.com
       /opt/qr-keeper/                qrkeeper.hackenshaw.com
       /opt/nextslide/server.js       nextslide.hackenshaw.com
       /opt/playground/server.js      playground.hackenshaw.com

   Link it root-relative so it stays same-origin on every vhost:
       <link rel="stylesheet" href="/assets/site.css?v=2">

   Every vhost carries a `location /assets/` alias block in
   /etc/nginx/sites-available/hackenshaw.com pointing back here —
   same shared-copy trick as the favicons. Bump the ?v=N on EVERY
   page when you edit this file, or the 4h Cloudflare edge cache
   serves the old one.

   ---- The one layout rule ----

   Every page is a fixed-viewport page. The page itself NEVER
   scrolls; exactly one region inside it does. Header and footer
   stay put, the middle gives. That shell lives here so no page
   has to rebuild it:

       html, body   pinned to the viewport, overflow hidden
       header/footer  flex-shrink: 0
       main         flex: 1; min-height: 0    <- load-bearing
       .panel       max-height: 100%

   The default scroll region is the panel itself:

       <div class="panel scroller">…</div>

   A page that needs something pinned inside the panel (a call to
   action, a preview) leaves the panel unscrolled, keeps it as the
   flex column it already is, and puts .scroller on the one child
   that should give — see cmdbox's <pre> and qr's .controls-col.

   NOT used by reveal.js decks — those have their own house theme
   in /presentations/theme/hackenshaw.css.
   ============================================================ */

/* The CRT look - scanlines, flicker, the glitching h1 - lives in its own
   file so the tutorials' hosted game pages can share it without taking the
   whole page shell. Every page gets it through this import. */
@import url("crt.css");

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

:root {
    --bg: #272727;
    --panel: #181818;
    --fg: chartreuse;
    --dim: #5a8f2a;
    --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    /* greyscale artwork -> chartreuse phosphor: project thumbnails, row logos */
    --tint: sepia(1) hue-rotate(52deg) saturate(2.4) brightness(0.95);
}

/* ---- The fixed-viewport shell ----
   height:100dvh comes second on purpose: on a phone 100vh is the height
   WITHOUT the collapsing address bar, so the footer sits off-screen until
   you scroll — the exact thing this shell exists to prevent. Browsers that
   do not know dvh ignore the second line and keep the first. */
html,
body {
    height: 100%;
    overflow: hidden;
    background-color: var(--bg);
    color: var(--fg);
    font-family: var(--mono);
}

body {
    display: flex;
    flex-direction: column;
    height: 100vh;
    height: 100dvh;
    padding: 3vh 5vw;
    gap: 2rem;
    -webkit-tap-highlight-color: transparent;
}

header,
footer {
    flex-shrink: 0;
}

a {
    color: inherit;
    text-decoration: none;
}

/* The one scroll region. Both rules are needed to hide the bar without
   losing the scrolling — neither alone covers every browser — and
   overscroll-behavior stops a flick past the end dragging the page behind it. */
.scroller {
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.scroller::-webkit-scrollbar {
    width: 0;
    height: 0;
    display: none;
}

/* ---- CRT overlay: in crt.css, imported at the top ---- */

/* ---- Header ----
   Sized for a page pinned to the viewport: a full-page header would eat
   most of a laptop screen and leave the panel a slot. */
header {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.9rem;
    text-align: center;
}

/* The header mark. It used to be a cropped photo, so it was circled, ringed
   and object-fit:cover; it is now the logo — angular artwork on transparency,
   which a circle would clip and a ring would fence in. Hence contain, no
   border, and the glow moved from box-shadow to drop-shadow so it follows the
   mark's own edge rather than a square box that is mostly empty.

   The class is still .avatar: it is the hook every page, both Node apps and
   the deck theme already use for "the mark in the header", and renaming it
   across five repos buys nothing.

   The logo carries ~10% internal padding and is taller than it is wide, so at
   the old 100px box it would read noticeably smaller than the circle did. The
   clamp is scaled up to match the previous optical weight, not the old numbers. */
.avatar {
    display: block;
    width: clamp(80px, 12.5vmin, 114px);
    aspect-ratio: 1;
    object-fit: contain;
    -webkit-user-drag: none;
    user-select: none;
    /* Kept deliberately faint. A box-shadow on the old circular portrait sat
       behind an opaque photo, so it could be strong; drop-shadow traces the
       logo's own outline, and anything heavier reads as a fringe drawn around
       the artwork rather than a glow behind it. */
    filter: drop-shadow(0 0 5px rgba(127, 255, 0, 0.14));
    transition: filter 0.18s ease;
}

/* the mark as a link home — used on every page except the landing page */
a.home {
    display: inline-flex;
    transition: transform 0.18s ease;
}

a.home:hover,
a.home:focus-visible {
    transform: scale(1.06);
    outline: none;
}

/* The hover glow belongs on the mark, not on a.home. A filter on the wrapper
   composes with the one on the image instead of replacing it, so the two
   drop-shadows stacked and bloomed hard enough to light up every edge in the
   artwork. One filter, one source of truth. */
a.home:hover .avatar,
a.home:focus-visible .avatar {
    filter: drop-shadow(0 0 9px rgba(127, 255, 0, 0.26));
}

h1 {
    font-size: clamp(1.7rem, 6vmin, 2.8rem);
    letter-spacing: 0.08em;
    font-weight: 700;
    text-transform: uppercase;
}

/* A project with its own logo puts it in the h1 in place of the avatar and
   the title (wip/maquette). The svg is inline and drawn in currentColor, so it
   takes the site's colour whatever colour the artwork was drawn in. The glow
   is the h1's "0 0 3px" text-shadow done as a filter: text-shadow reaches
   svg <text> but not lines, so it would light the lettering and not the art. */
h1.wordmark svg {
    display: block;
    width: clamp(170px, 30vmin, 250px);
    height: auto;
    color: var(--fg);
    filter: drop-shadow(0 0 3px rgba(127, 255, 0, 0.55));
}

.tagline {
    color: var(--dim);
    font-size: clamp(0.78rem, 2.2vmin, 0.95rem);
    letter-spacing: 0.05em;
}

/* ---- Terminal panel ----
   min-height:0 on <main> is the load-bearing line — without it a flex item
   refuses to shrink below its content and the panel pushes the footer
   off-screen instead of scrolling. */
main {
    flex: 1;
    min-height: 0;
    display: flex;
    justify-content: center;
}

.panel {
    background: var(--panel);
    border-radius: 25px;
    padding: clamp(1.2rem, 3.5vmin, 2.2rem);
    width: 100%;
    max-width: 780px;
    max-height: 100%;
    box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
    /* a column by default, so a page can pin one child and let another give */
    display: flex;
    flex-direction: column;
    /* a flex item's default min-width:auto would let one long nowrap line
       (an embed snippet, a code block) push the panel wider than the page */
    min-width: 0;
}

/* When the panel IS the scroll region it goes back to being a plain block:
   as a flex column its children would squash to fit instead of overflowing,
   and nothing would ever scroll. */
.panel.scroller {
    display: block;
}

.prompt {
    color: var(--dim);
    font-size: clamp(0.78rem, 2vmin, 0.95rem);
    margin-bottom: 1.4rem;
    letter-spacing: 0.03em;
}

.prompt span {
    color: var(--fg);
}

.cursor {
    display: inline-block;
    width: 0.55em;
    height: 1em;
    background: var(--fg);
    vertical-align: text-bottom;
    animation: blink 1.1s steps(1) infinite;
}

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

/* ---- The list of things ----
   One shape for every listing page. The <li> is the bordered row;
   a.entry is the main link inside it; a.dl is an optional trailing
   icon button (used by the decks list):

       <ul class="list">
         <li>
           <a class="entry" href="…">
             <span class="title">name</span>
             <span class="desc">what it is</span>
           </a>
           <a class="dl" href="…">…optional icon…</a>
         </li>
       </ul>
*/
ul.list {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0.85rem;
}

ul.list li {
    display: flex;
    align-items: center;
    border: 1px solid rgba(127, 255, 0, 0.25);
    border-radius: 12px;
    transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
}

ul.list li:hover,
ul.list li:focus-within {
    border-color: var(--fg);
    background: rgba(127, 255, 0, 0.07);
    transform: translateX(4px);
    outline: none;
}

/* the row's padding lives on the link, not the <li>, so the whole
   box is clickable and not just the text inside it */
ul.list a.entry {
    display: flex;
    align-items: baseline;
    gap: 0.9rem;
    flex-wrap: wrap;
    flex: 1;
    min-width: 0;
    padding: 0.85rem 1rem;
    outline: none;
}

.title {
    font-size: clamp(0.95rem, 2.6vmin, 1.15rem);
    font-weight: 700;
    letter-spacing: 0.02em;
}

.title::before {
    content: "→ ";
    color: var(--dim);
}

.desc {
    color: var(--dim);
    font-size: clamp(0.75rem, 2vmin, 0.9rem);
    flex: 1;
    min-width: 12ch;
}

/* trailing icon button in a row, right-aligned */
ul.list a.dl {
    display: flex;
    flex: none;
    padding: 0.45rem;
    /* matches the 1rem right padding the <li> used to carry */
    margin-right: 1rem;
    border-radius: 8px;
    color: var(--dim);
    transition: color 0.18s ease, filter 0.18s ease, transform 0.18s ease;
}

ul.list a.dl:hover,
ul.list a.dl:focus-visible {
    color: var(--fg);
    filter: drop-shadow(0 0 8px rgba(127, 255, 0, 0.7));
    transform: translateY(2px);
    outline: none;
}

ul.list a.dl svg {
    display: block;
    width: 20px;
    height: 20px;
    fill: currentColor;
}

/* two buttons in a row (download + gefontyst download) sit as a pair */
ul.list a.dl:has(+ a.dl) {
    margin-right: 0.15rem;
}

/* An icon that only exists as a raster - another brand's mark - drawn in
   currentColor like the svg icons, through a mask: <span class="mask
   fontys"></span>. The png is an alpha mask, its colour is ignored. */
ul.list a.dl .mask {
    display: block;
    width: 20px;
    height: 20px;
    background: currentColor;
    -webkit-mask: var(--mask) center / contain no-repeat;
    mask: var(--mask) center / contain no-repeat;
}

/* Drawn larger than the 20px arrow beside it (a thin mark reads small), with
   the negative margin keeping the button the same size as its neighbour.
   Lifted 3px: the ink is centred, but the heavy fish sits on top of a thin
   F, so true centre looks low next to the arrow. */
ul.list a.dl .mask.fontys {
    --mask: url("/assets/fontys-mark.png");
    width: 26px;
    height: 26px;
    margin: -3px;
    transform: translateY(-3px);
}

/* The button's label on hover: <a class="dl" data-tip="Download ...">.
   A native title tooltip takes a second to appear and cannot be styled,
   so the label is drawn here, above the button and flush with its right
   edge - beside it, it would cover the button next to it in a pair. */
ul.list a.dl[data-tip] {
    position: relative;
}

ul.list a.dl[data-tip]::after {
    content: attr(data-tip);
    position: absolute;
    right: 0;
    bottom: calc(100% + 0.35rem);
    padding: 0.3rem 0.6rem;
    white-space: nowrap;
    font-size: clamp(0.7rem, 1.8vmin, 0.8rem);
    color: var(--fg);
    background: var(--panel);
    border: 1px solid rgba(127, 255, 0, 0.25);
    border-radius: 8px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease;
}

ul.list a.dl[data-tip]:hover::after,
ul.list a.dl[data-tip]:focus-visible::after {
    opacity: 1;
}

/* A decorative logo or icon at the end of a row (wip, tutorials). It sits
   outside a.entry, like a.dl, so the text column shrinks against it instead
   of wrapping it onto its own line; it repeats the entry's href only so the
   far right of the row stays clickable, and is hidden from assistive tech
   and skipped by the keyboard:

       <a class="rowlogo" href="…" tabindex="-1" aria-hidden="true">
         <img src="…greyscale…">   or   <svg>…</svg>
       </a>

   A greyscale <img> is tinted by --tint; an inline <svg> is drawn in
   currentColor. */
ul.list a.rowlogo {
    display: flex;
    flex: none;
    align-items: center;
    margin-right: 1rem;
    color: var(--fg);
}

.rowlogo img,
.rowlogo svg {
    display: block;
    width: clamp(26px, 6vmin, 34px);
    height: auto;
    transition: filter 0.18s ease;
}

.rowlogo img { filter: var(--tint); }
.rowlogo svg { fill: currentColor; }

ul.list li:hover .rowlogo img,
ul.list li:focus-within .rowlogo img {
    filter: var(--tint) drop-shadow(0 0 8px rgba(127, 255, 0, 0.7));
}

ul.list li:hover .rowlogo svg,
ul.list li:focus-within .rowlogo svg {
    filter: drop-shadow(0 0 8px rgba(127, 255, 0, 0.7));
}

/* ---- A group of rows: a folder in the listing ----
   Related entries sit indented under one label, the way `tree` would draw
   a directory. The group is itself an <li> of the outer list, so it takes
   its place in the order, but it is not a row: no border, no hover. Its
   rows are an ordinary ul.list and behave like every other row.

       <li class="group">
         <p class="group-label">game design</p>
         <ul class="list">
           <li>…row…</li>
         </ul>
       </li>
*/
ul.list li.group {
    display: block;
    border: none;
}

ul.list li.group:hover,
ul.list li.group:focus-within {
    background: none;
    transform: none;
}

.group-label {
    font-size: clamp(0.95rem, 2.6vmin, 1.15rem);
    font-weight: 700;
    letter-spacing: 0.02em;
    margin-bottom: 0.7rem;
}

/* A directory in `ls -F`: the trailing slash says "there is more in here".
   No leading arrow - that marks a link, and the label is not one. */
.group-label::after {
    content: "/";
    color: var(--dim);
}

/* the tab: indented, with a rule down the left like a tree's trunk */
ul.list li.group > ul.list {
    margin-left: 0.55rem;
    padding-left: 1.2rem;
    border-left: 1px solid rgba(127, 255, 0, 0.25);
}

/* ---- Empty state ---- */
.empty {
    border: 1px dashed rgba(127, 255, 0, 0.3);
    border-radius: 12px;
    padding: clamp(1.4rem, 4vmin, 2.2rem) 1rem;
    text-align: center;
    color: var(--dim);
    font-size: clamp(0.8rem, 2.2vmin, 0.95rem);
    line-height: 1.9;
}

.empty strong,
.doc strong {
    color: var(--fg);
    font-weight: 700;
    letter-spacing: 0.04em;
}

/* ---- Counts ----
   An aligned label/number table, the way it would print in a terminal:
   a fixed label column so the numbers stack, and a narrow number column
   so 2 and 19 right-align against each other. Used by /status/ and
   /wip/godot/.

       <dl class="tally">
         <dt>projects</dt><dd>4</dd>
       </dl>

   16ch fits the longest label in use ("presentations", 13). A longer
   label wants a wider column — change it here, not on the page.
*/
.tally {
    display: grid;
    grid-template-columns: 16ch max-content;
    row-gap: 0.55rem;
    font-size: clamp(0.85rem, 2.3vmin, 1rem);
    margin-bottom: 2.2rem;
}

.tally dt {
    color: var(--dim);
    letter-spacing: 0.03em;
}

.tally dd {
    color: var(--fg);
    font-weight: 700;
    min-width: 3ch;
    text-align: right;
}

/* ---- A briefing with a pinned call to action ----
   cmdbox and /wip/maquette/: the panel stays the flex column, the prose
   scrolls and the button under it never moves.

       <div class="panel">
         <div class="briefing scroller">
           <p class="prompt">$ <span>cat README</span></p>
           <p>…</p>
           <dl class="rules"><dt>a claim</dt><dd>why</dd></dl>
         </div>
         <a class="cta" href="…">OPEN THE THING &rarr;</a>
       </div>

   flex:1 takes the space the button leaves; min-height:0 is what lets it
   shrink and scroll instead of pushing the button off the panel. The
   hairline is on the border box, so it stays put while the prose moves
   under it - text scrolling out of view reads as cut off, not as the end.
   A button with nowhere to go yet is an <a> with no href and
   aria-disabled="true": dim and dashed, and it does not light up. */
.panel:has(> .cta) { gap: 1.1rem; }

.briefing {
    flex: 1;
    min-height: 0;
    padding-bottom: 1rem;
    border-bottom: 1px solid rgba(127, 255, 0, 0.15);
    font-size: clamp(0.8rem, 2.1vmin, 0.95rem);
    line-height: 1.7;
}

.briefing p { color: var(--dim); }
.briefing p + p { margin-top: 0.9rem; }

/* The "$ cat X" lines are section headings here, not one prompt at the
   top, so they need air above as well as below - except the first, which
   is already against the panel's padding. */
.briefing .prompt { margin: 1.8rem 0 0.8rem; }
.briefing .prompt:first-child { margin-top: 0; }
.briefing .prompt:last-child { margin-bottom: 0; }

/* Each rule is a bright claim with a dim explanation under it. */
.rules dt { color: var(--fg); font-weight: 700; }
.rules dd { color: var(--dim); margin: 0.2rem 0 0.9rem 1.2rem; }
.rules dd:last-child { margin-bottom: 0; }

.cta {
    flex-shrink: 0;
    display: block;
    text-align: center;
    padding: 0.85rem 1.2rem;
    border: 1px solid var(--fg);
    border-radius: 999px;
    letter-spacing: 0.06em;
    font-weight: 700;
    transition: background 0.18s ease, box-shadow 0.18s ease;
}

.cta:hover,
.cta:focus-visible {
    background: rgba(127, 255, 0, 0.1);
    box-shadow: 0 0 14px rgba(127, 255, 0, 0.35);
    outline: none;
}

.cta[aria-disabled="true"] {
    color: var(--dim);
    border: 1px dashed rgba(127, 255, 0, 0.3);
    background: none;
    box-shadow: none;
    cursor: default;
}

/* A back link under the button: the panel's gap already spaces it. */
.panel:has(> .cta) > .back {
    margin-top: 0;
}

/* ---- Document pages: the qrkeeper format ----
   <body class="crt-overlay doc-page">: the tutorials. A long read wants the
   panel, not the header, so these take the same shape as qrkeeper.hackenshaw.com
   - the header trimmed to a small mark and title, the page's padding and gaps
   cut down, and the panel widened to 900px. Values are qrkeeper's
   (/opt/qr-keeper/css/style.css, which inlines its own sheet by design);
   change one, change both. Listing pages keep the full header. */
body.doc-page {
    padding: 1.6vh 3vw;
    gap: 0.8rem;
}

body.doc-page header { gap: 0.25rem; }

body.doc-page .avatar { width: clamp(39px, 5.7vmin, 57px); }

body.doc-page h1 { font-size: clamp(1rem, 3.2vmin, 1.4rem); }

body.doc-page .tagline { font-size: clamp(0.66rem, 1.6vmin, 0.8rem); }

body.doc-page .panel {
    max-width: 900px;
    padding: clamp(0.8rem, 2.4vmin, 1.4rem);
}

/* ---- A written document: the tutorials ----
   Long-form prose inside the panel, which is itself the scroller
   (.panel.scroller), so a contents list at the top jumps within the panel.

       <article class="doc">
         <nav class="toc">…</nav>
         <h2 id="step-1">Step 1 — …</h2>
         <p>…</p>
         <p class="file">game.js</p>
         <pre><code>…<span class="add">a new line</span>…<span class="c">// a comment</span></code></pre>
         <figure><img …><figcaption>…</figcaption></figure>
         <div class="tip"><p>…</p></div>
       </article>

   Code wraps rather than scrolling sideways: the page has exactly one scroll
   region, and a second one inside a code block is the thing that rule is
   there to prevent. */
/* Running text is dim and the bright phosphor is kept for what matters -
   headings, links, emphasis, code - exactly like the cmdbox briefing and
   every other page's prose. */
.doc {
    font-size: clamp(0.82rem, 2.2vmin, 0.95rem);
    line-height: 1.75;
    color: var(--dim);
}

.doc h2,
.doc h3,
.doc a {
    color: var(--fg);
}

.doc > :first-child { margin-top: 0; }

.doc h2 {
    font-size: clamp(1.05rem, 2.9vmin, 1.3rem);
    letter-spacing: 0.02em;
    line-height: 1.35;
    margin: 2.6rem 0 0.9rem;
    padding-top: 1.6rem;
    border-top: 1px solid rgba(127, 255, 0, 0.15);
    scroll-margin-top: 1rem;
}

.doc h3 {
    font-size: 1em;
    letter-spacing: 0.04em;
    margin: 1.8rem 0 0.6rem;
}

.doc p + p { margin-top: 0.9rem; }

.doc ul,
.doc ol {
    margin: 0.8rem 0 0.8rem 1.4rem;
}

.doc ul { list-style: none; }

.doc ul > li { position: relative; }

.doc ul > li::before {
    content: "→";
    position: absolute;
    left: -1.4rem;
    color: var(--dim);
}

.doc ol > li::marker { color: var(--dim); }

.doc li + li { margin-top: 0.35rem; }

.doc a {
    text-decoration: underline;
    text-decoration-color: rgba(127, 255, 0, 0.4);
    text-underline-offset: 0.2em;
}

.doc a:hover,
.doc a:focus-visible { text-decoration-color: var(--fg); outline: none; }

/* Emphasis: the same rule as .empty strong, further up - phosphor, bold,
   a touch of tracking. Never white: the site is one colour. */

.doc :not(pre) > code {
    background: rgba(127, 255, 0, 0.08);
    border-radius: 4px;
    padding: 0.05em 0.35em;
    font-size: 0.95em;
}

/* the file a code block belongs to, as a tab sitting on top of it */
.doc .file {
    display: inline-block;
    margin: 1.2rem 0 0;
    padding: 0.15rem 0.8rem;
    border: 1px solid rgba(127, 255, 0, 0.2);
    border-bottom: 0;
    border-radius: 8px 8px 0 0;
    color: var(--dim);
    font-size: 0.85em;
}

.doc pre {
    margin: 0.9rem 0;
    padding: 1rem 1.1rem;
    background: #0f0f0f;
    border: 1px solid rgba(127, 255, 0, 0.2);
    border-radius: 12px;
    font-size: 0.88em;
    line-height: 1.6;
    white-space: pre-wrap;
    word-break: break-word;
    /* GDScript indents with tabs; 8 columns each, the browser default, is
       twice what any editor shows */
    tab-size: 4;
    color: var(--dim);
}

.doc .file + pre {
    margin-top: 0;
    border-top-left-radius: 0;
}

/* Lines that are new in this step are the bright ones; everything already
   typed in earlier steps stays dim, so the eye goes straight to what to add. */
.doc pre .add { color: var(--fg); }
.doc pre .add .c,
.doc pre .c { color: #3f6520; }

.doc figure {
    margin: 1.2rem 0;
    text-align: center;
}

.doc figure img,
.doc figure svg {
    display: block;
    margin: 0 auto;
    max-width: 100%;
    height: auto;
    border-radius: 12px;
    border: 1px solid rgba(127, 255, 0, 0.2);
}

.doc figure img.narrow { max-width: 420px; width: 100%; }

/* a live demo in place of a picture: the finished game, playable on the page */
.doc figure iframe {
    display: block;
    width: 100%;
    max-width: 520px;
    height: 600px;
    margin: 0 auto;
    border: 1px solid rgba(127, 255, 0, 0.25);
    border-radius: 12px;
    background: var(--bg);
}

/* Screenshots sit above the CRT overlay (z-index 2): scanlines over a
   picture of someone else's interface only make it harder to read. The
   prose around them keeps the house screen. Nothing between here and the
   root forms a stacking context, so this z-index competes with the overlay
   directly. */
.doc figure img {
    position: relative;
    z-index: 3;
}

.doc figcaption {
    margin-top: 0.6rem;
    color: var(--dim);
    font-size: 0.85em;
    line-height: 1.6;
}

.doc .tip {
    margin: 1.2rem 0;
    padding: 0.8rem 1rem;
    border-left: 2px solid var(--fg);
    background: rgba(127, 255, 0, 0.05);
    border-radius: 0 10px 10px 0;
    color: var(--dim);
}

.doc .tip strong { color: var(--fg); }

/* "run this step" and friends: a row of small pill links */
.doc .run {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0.8rem 0;
}

.doc .run a {
    text-decoration: none;
    padding: 0.2rem 0.8rem;
    border: 1px solid rgba(127, 255, 0, 0.3);
    border-radius: 999px;
    font-size: 0.85em;
    transition: border-color 0.18s ease, background 0.18s ease;
}

.doc .run a:hover,
.doc .run a:focus-visible {
    border-color: var(--fg);
    background: rgba(127, 255, 0, 0.08);
}

.doc .toc {
    margin: 1rem 0 0;
    padding: 0.9rem 1.1rem;
    border: 1px dashed rgba(127, 255, 0, 0.3);
    border-radius: 12px;
}

.doc .toc ol { margin: 0.3rem 0 0 1.4rem; }

/* ---- Notes and back link ---- */
.note {
    color: var(--dim);
    font-size: clamp(0.72rem, 1.9vmin, 0.85rem);
    line-height: 1.7;
    margin-bottom: 1.4rem;
}

.note code {
    color: var(--fg);
}

.back {
    display: block;
    margin-top: 1.2rem;
    color: var(--dim);
    font-size: clamp(0.75rem, 2vmin, 0.9rem);
    letter-spacing: 0.03em;
    transition: color 0.18s ease;
}

.back:hover,
.back:focus-visible {
    color: var(--fg);
    outline: none;
}

/* ---- The word NEXT ------------------------------------------------------
   Brand rule: wherever the word NEXT appears as the name of the thing — the
   button on nextslide.hackenshaw.com/click, the Next Slide Please deck — it
   is set in the same pixel face as the QR KEEPER title, not in the body
   mono. Everywhere else "next" is just a word and stays as it is.

       <span class="nextword">NEXT</span>

   The face is vendored at /assets/fonts/ rather than pulled from Google
   Fonts: every vhost aliases /assets/, so one copy serves all of them
   same-origin, and nothing on the page waits on a third party. Press Start
   2P is OFL-1.1; the licence sits beside the file.

   Press Start 2P has unusual vertical metrics, and both of the obvious
   symptoms come straight out of them:

       unitsPerEm 1000   ascent 1000   descent 0   capHeight 1000
       cap ink spans y +0.125 .. +1.000 em

   Its capitals do NOT sit on the baseline — they float 0.125em above it —
   and the face has no descent at all, so its whole inline box is above the
   baseline.

   font-size: the visual cap is (1.000 - 0.125) = 0.875 of the font size, so
   0.8em lands the cap on a 0.70em monospace cap height.

   vertical-align: -0.125em lowers it by 0.125 of its OWN size, which is the
   exact ink offset, dropping the capitals onto the surrounding baseline.
   ------------------------------------------------------------------------ */
@font-face {
    font-family: "Press Start 2P";
    src: url("/assets/fonts/press-start-2p.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

.nextword {
    font-family: "Press Start 2P", var(--mono);
    font-size: 0.8em;
    vertical-align: -0.125em;
    letter-spacing: 0.06em;
    white-space: nowrap;
    text-transform: uppercase;
    /* Never dimmed: the word is the brand wherever it appears, including
       inside dim running text like the keyboard readout. */
    color: var(--fg);
    /* The glow is 0.5em of the word's own size, which is the ratio the
       QR KEEPER title uses (12px on a 24px face). In px it read as a
       different weight of glow at every size the word appears in, which is
       what made it look brighter in the deck than on the click page. */
    text-shadow: 0 0 0.5em rgba(127, 255, 0, 0.6);
    /* the deck animates a chromatic-aberration shadow on h1/h2; the brand
       stays still so it reads the same here as anywhere else */
    animation: none;
}

/* ---- Footer ----
   The footer markup stays inline in each page on purpose (static
   HTML has no include). These styles are shared; if you change the
   social links, edit every page's <footer>.
*/
footer {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1.4rem;
    padding-bottom: 1vh;
}

footer a {
    display: flex;
    padding: 0.35rem;
    border-radius: 8px;
    transition: transform 0.18s ease, filter 0.18s ease;
}

footer a:hover,
footer a:focus-visible {
    transform: translateY(-3px);
    filter: drop-shadow(0 0 8px rgba(127, 255, 0, 0.7));
    outline: none;
}

footer svg {
    width: 26px;
    height: 26px;
    fill: var(--fg);
}

@media (prefers-reduced-motion: reduce) {

    .cursor {
        animation: none;
    }
}
