/* ============================================================
   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

   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.
   ============================================================ */

* {
    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;
}

/* ---- 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: scanlines + flicker ---- */
.crt-overlay::before,
.crt-overlay::after {
    content: " ";
    display: block;
    position: fixed;
    inset: 0;
    z-index: 2;
    pointer-events: none;
}

.crt-overlay::before {
    background:
        linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
        linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
    background-size: 100% 3px, 3px 100%;
}

.crt-overlay::after {
    background: rgba(18, 16, 16, 0.1);
    opacity: 0;
    animation: flicker 0.15s infinite;
}

@keyframes flicker {
    0% { opacity: 0.28; }
    15% { opacity: 0.91; }
    25% { opacity: 0.84; }
    40% { opacity: 0.27; }
    50% { opacity: 0.96; }
    60% { opacity: 0.20; }
    75% { opacity: 0.37; }
    90% { opacity: 0.70; }
    100% { opacity: 0.24; }
}

@keyframes textShadow {
    0% { text-shadow: 0.44px 0 1px rgba(0, 30, 255, 0.5), -0.44px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px; }
    25% { text-shadow: 1.61px 0 1px rgba(0, 30, 255, 0.5), -1.61px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px; }
    50% { text-shadow: 0.08px 0 1px rgba(0, 30, 255, 0.5), -0.08px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px; }
    75% { text-shadow: 1.89px 0 1px rgba(0, 30, 255, 0.5), -1.89px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px; }
    100% { text-shadow: 2.62px 0 1px rgba(0, 30, 255, 0.5), -2.62px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px; }
}

/* ---- 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;
    animation: textShadow 1.6s infinite;
}

.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;
}

/* ---- 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 {
    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 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. */
.doc {
    font-size: clamp(0.82rem, 2.2vmin, 0.95rem);
    line-height: 1.75;
}

.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;
    color: var(--dim);
    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; }

.doc strong { color: #fff; }

.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;
    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%; }

/* 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) {

    .crt-overlay::after,
    h1,
    .cursor {
        animation: none;
    }
}
