/* ── The typeface ─────────────────────────────────────────────────────────────

   Ioskeley Mono, self-hosted, replacing Fira Code everywhere. OFL 1.1; the
   licence travels with the files in static/fonts/ because the licence requires
   it to.

   The faces are declared here rather than in the theme because the theme is a
   submodule: `themes/terminus/sass/css/_fonts.scss` still declares Fira Code
   and still ships those files, and overriding from the site's own stylesheet
   keeps the swap in this repo. The family is overridden rather than the
   @font-face redefined under the old name — a rule that says `Fira Code` and
   loads something else is a trap for whoever reads it next.

   Bold is declared at 700, not the theme's 800. Terminus declares its bold
   face at 800 while every weight in the system asks for 700, so the browser
   was synthesising nothing and simply picking the 800 file. Declaring the
   weight that is actually asked for means the right file is matched rather
   than fallen back to.                                                       */
@font-face {
    font-family: 'Ioskeley Mono';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/fonts/IoskeleyMono-Regular.woff2') format('woff2');
}

@font-face {
    font-family: 'Ioskeley Mono';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('/fonts/IoskeleyMono-Bold.woff2') format('woff2');
}

/* One family on every surface, including the places terminus names its own
   stack: body, code blocks and inline code. */
:root:has(body[data-theme="terminus"]) body,
body,
pre,
code,
kbd,
samp,
textarea,
input,
button,
select {
    font-family: 'Ioskeley Mono', Monaco, Consolas, 'Ubuntu Mono', monospace;
}

:root:has(body[data-theme="terminus"]) {
    --accent-color: #ff00ff !important;
}

/* The rift sits behind every page. Terminus makes <body> itself the 864px centred
   column, so body::before is exactly the content column and nothing else — the rift
   stays at full strength in the margins either side.
   html needs its own background: without one, body's background propagates to the root
   canvas and paints below everything, including the negative-z-index layers below. */
html {
    background: #000;
}

body {
    position: relative;
    background: transparent;
}

body::before {
    content: "";
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.82);
    z-index: -1;
}

/* ── The home surface: the aperture ───────────────────────────────────────────

   `adimension` is a window onto the rift, not coloured text. One in-flow SVG
   paints black across its own box with the word cut out of it as a mask, so the
   canvas at z-index -2 reaches the eye through the letterforms and nowhere
   else. Two consequences worth knowing before editing anything below:

   1. Nothing that is an ancestor of that SVG may carry a background. <main> and
      <body> stay transparent; a background on either one paints under the SVG
      and fills the hole with its own colour. Every sibling region paints its
      own black instead, and body::after absorbs whatever height is left.
   2. `meet` plus a mask rect drawn far outside the viewBox means the word
      scales with the viewport and can never be cropped. That is also the fix
      for the old landing page, where a clamp() headline overflowed every
      phone-width viewport by six pixels.                                     */

/* ── One frame, every route ───────────────────────────────────────────────────

   There is no per-route layout any more. The site used to carry three: the home
   page's full-bleed aperture, a "manual" frame for the reading routes, and a
   left rail for the lists. The rail existed only because the old field was a
   left-edge phenomenon and that column was the only part worth showing; the
   tear is composed around the masthead band instead, so the premise is gone and
   both extra frames went with it.

   The ancestor rule survives and is still the most breakable thing here:
   nothing above the aperture SVG in the tree may carry a background, or it
   paints under the mask and fills the cut. <body> and <header> stay
   transparent; every sibling region paints its own black. */

/* `body[class]` rather than `body`: the theme sets the centred reading column
   with a class selector, so a bare element selector loses the cascade. Every
   page carries a layout- class, so this matches all of them. */
body[class] {
    max-width: 100%;
    padding: 0;
    border: none;
}

/* The theme's reading veil. The aperture needs the field at full strength and
   the page paints its own black anyway, so it has nothing left to dim. */
body::before {
    content: none;
}

/* Absorbs whatever viewport height is left below the content, so a short page
   does not show raw field under its own footer. */
body::after {
    content: "";
    flex: 1 1 auto;
    background: #000;
}

body > main,
body > .footer,
.site-rail,
.masthead-slot {
    background: #000;
}

body > .header {
    background: transparent;
    /* The theme centres and shrink-wraps header children for its own
       logo-and-menu row. The aperture is the masthead and must span the page. */
    align-items: stretch;
    padding: 0;
    max-width: 100%;
    /* The theme flips .header to a row below 684px for its own logo-and-menu
       layout, which would collapse the aperture into a narrow flex item. */
    flex-direction: column;
}

.aperture-link {
    display: block;
    width: 100%;
    text-decoration: none;
}

.aperture {
    display: block;
    width: 100%;
    height: auto;
}

/* The rail. Quiet on purpose: the aperture is the masthead, so nav must not
   compete with it. No chips, no boxes — the interior pages own that language. */
.site-rail {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 24px 40px;
    flex-wrap: wrap;
    padding: 26px 40px 20px;
}

.site-rail ul {
    display: flex;
    flex-wrap: wrap;
    gap: 10px 28px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.site-rail a {
    color: rgba(245, 245, 245, 0.72);
    font-size: 0.82rem;
    letter-spacing: 0.06em;
    text-decoration: none;
    padding-bottom: 2px;
    border-bottom: 1px solid transparent;
    transition: color 0.25s ease, border-color 0.25s ease;
}

.site-rail a:hover,
.site-rail a:focus-visible,
.site-rail a[aria-current="page"] {
    color: #fff;
    border-bottom-color: var(--accent-color);
}

/* Hangs off the end of the word rather than sitting centred beneath it. The
   home page says what the site is; every other route says where you are. */
.masthead-slot {
    margin: 0;
    padding: 0 40px 44px;
    text-align: right;
    font-size: 0.8rem;
    letter-spacing: 0.42em;
    text-transform: lowercase;
    color: rgba(245, 245, 245, 0.55);
}

/* The footer index. Categories and tags live here rather than in the menu:
   they are routes through an archive you are already in, not destinations. */
.footer-index {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px 28px;
    margin: 0 0 14px;
}

.footer-index a {
    color: rgba(245, 245, 245, 0.5);
    font-size: 0.82rem;
    letter-spacing: 0.06em;
    text-decoration: none;
    padding-bottom: 2px;
    border-bottom: 1px solid transparent;
    transition: color 0.25s ease, border-color 0.25s ease;
}

.footer-index a:hover,
.footer-index a:focus-visible {
    color: #fff;
    border-bottom-color: var(--accent-color);
}

/* The ledger. One row per post: date, title, then category and reading time
   pushed to the far edge, on a hairline rule. The hue marker in the left gutter
   is the same per-post hue the blog list uses for its spine. */
.ledger {
    margin: 0;
    padding: 0 40px 56px;
    list-style: none;
    counter-reset: none;
}

.ledger-row {
    position: relative;
    border-top: 1px solid rgba(245, 245, 245, 0.1);
}

.ledger-row:last-child {
    border-bottom: 1px solid rgba(245, 245, 245, 0.1);
}

/* The marker carries the per-post hue, the one taxonomy signal in the world, so
   it has to carry that hue *at rest* — a touch device never sees a hover state,
   and a neutral tick there means the signal is a fact the markup knows and the
   screen never shows.

   Lightness 72%, not the 60% the blog spine used to use: at 60% a blue post
   (hue 240) lands at 1.9:1 against the plate, under the 3:1 floor for a
   non-text graphic. 72% at 0.85 alpha puts the worst hue at about 4:1 and
   leaves the rest comfortably above it. Length, not dimness, is what keeps the
   resting state quiet. */
.ledger-row::before {
    content: "";
    position: absolute;
    left: -16px;
    top: 50%;
    width: 2px;
    height: 20px;
    background: hsl(var(--hue, 300) 100% 72%);
    opacity: 0.85;
    transform: translateY(-50%);
    transition: height 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s ease, box-shadow 0.4s ease;
}

.ledger-row:hover::before,
.ledger-row:focus-within::before {
    height: 100%;
    opacity: 1;
    box-shadow: 0 0 10px hsl(var(--hue, 300) 100% 72%);
}

/* One basis for the date column, used by both the column itself and the
   summary's indent. Two `ch` counts at two font sizes is how the summary ended
   up starting 10px left of its own title. */
.ledger {
    --ledger-date: 84.5px;
}

.ledger-row a {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 28px;
    padding: 18px 0 4px;
    text-decoration: none;
    color: inherit;
}

.ledger-row time {
    flex: none;
    width: var(--ledger-date);
    font-size: 0.8rem;
    color: rgba(245, 245, 245, 0.5);
    font-variant-numeric: tabular-nums;
}

.ledger-title {
    flex: 1 1 auto;
    font-size: 1.05rem;
    line-height: 1.45;
    color: #f5f5f5;
    transition: color 0.25s ease;
}

.ledger-row:hover .ledger-title,
.ledger-row:focus-within .ledger-title {
    color: hsl(var(--hue, 300) 100% 72%);
}

.ledger-tail {
    flex: none;
    display: flex;
    gap: 20px;
    font-size: 0.78rem;
    color: rgba(245, 245, 245, 0.5);
}

.ledger-time {
    width: 6ch;
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.ledger-summary {
    flex: 1 0 100%;
    margin: 0;
    padding: 2px 0 18px calc(var(--ledger-date) + 28px);
    font-size: 0.86rem;
    line-height: 1.6;
    color: rgba(245, 245, 245, 0.62);
}

@media (max-width: 760px) {
    .site-rail,
    .masthead-slot,
    .ledger {
        padding-inline: 20px;
    }

    .ledger-row a {
        gap: 4px 20px;
    }

    .ledger-title {
        flex-basis: 100%;
        order: 3;
    }

    .ledger-summary {
        order: 4;
        padding-left: 0;
    }
}

.grain-overlay {
    position: fixed;
    inset: 0;
    z-index: 100;
    pointer-events: none;
}

/* Ensure any other elements that might be using the default color fallback to magenta too */
::selection {
    background: #ff00ff;
    color: #000;
}


/* Soundwave Header Bar: Morse code bars for "adimension", full colour spectrum.
   The morse pattern is the mask (alpha stencil); the background underneath is a
   hue-wheel gradient sized to one 202px pattern repeat. soundwave-drift animates
   background-position, so the spectrum flows through the static bars. */
@keyframes soundwave-drift {
    0% {
        background-position: 0 0;
    }
    100% {
        background-position: 202px 0;
    }
}

.header-logo-container::after {
    background: linear-gradient(
        90deg,
        hsl(0, 90%, 62%), hsl(45, 90%, 58%), hsl(90, 80%, 55%),
        hsl(135, 75%, 55%), hsl(180, 80%, 55%), hsl(225, 85%, 62%),
        hsl(270, 85%, 65%), hsl(315, 90%, 62%), hsl(360, 90%, 62%)
    ) !important;
    background-size: 202px 100% !important;
    -webkit-mask-image: repeating-linear-gradient(
        to right,
        #000 0px, #000 2px, transparent 2px, transparent 5px,
        #000 5px, #000 11px, transparent 11px, transparent 19px,
        #000 19px, #000 25px, transparent 25px, transparent 28px,
        #000 28px, #000 30px, transparent 30px, transparent 33px,
        #000 33px, #000 35px, transparent 35px, transparent 43px,
        #000 43px, #000 45px, transparent 45px, transparent 48px,
        #000 48px, #000 50px, transparent 50px, transparent 58px,
        #000 58px, #000 64px, transparent 64px, transparent 67px,
        #000 67px, #000 73px, transparent 73px, transparent 81px,
        #000 81px, #000 83px, transparent 83px, transparent 91px,
        #000 91px, #000 97px, transparent 97px, transparent 100px,
        #000 100px, #000 102px, transparent 102px, transparent 110px,
        #000 110px, #000 112px, transparent 112px, transparent 115px,
        #000 115px, #000 117px, transparent 117px, transparent 120px,
        #000 120px, #000 122px, transparent 122px, transparent 130px,
        #000 130px, #000 132px, transparent 132px, transparent 135px,
        #000 135px, #000 137px, transparent 137px, transparent 145px,
        #000 145px, #000 151px, transparent 151px, transparent 154px,
        #000 154px, #000 160px, transparent 160px, transparent 163px,
        #000 163px, #000 169px, transparent 169px, transparent 177px,
        #000 177px, #000 183px, transparent 183px, transparent 186px,
        #000 186px, #000 188px, transparent 188px, transparent 202px
    );
    mask-image: repeating-linear-gradient(
        to right,
        #000 0px, #000 2px, transparent 2px, transparent 5px,
        #000 5px, #000 11px, transparent 11px, transparent 19px,
        #000 19px, #000 25px, transparent 25px, transparent 28px,
        #000 28px, #000 30px, transparent 30px, transparent 33px,
        #000 33px, #000 35px, transparent 35px, transparent 43px,
        #000 43px, #000 45px, transparent 45px, transparent 48px,
        #000 48px, #000 50px, transparent 50px, transparent 58px,
        #000 58px, #000 64px, transparent 64px, transparent 67px,
        #000 67px, #000 73px, transparent 73px, transparent 81px,
        #000 81px, #000 83px, transparent 83px, transparent 91px,
        #000 91px, #000 97px, transparent 97px, transparent 100px,
        #000 100px, #000 102px, transparent 102px, transparent 110px,
        #000 110px, #000 112px, transparent 112px, transparent 115px,
        #000 115px, #000 117px, transparent 117px, transparent 120px,
        #000 120px, #000 122px, transparent 122px, transparent 130px,
        #000 130px, #000 132px, transparent 132px, transparent 135px,
        #000 135px, #000 137px, transparent 137px, transparent 145px,
        #000 145px, #000 151px, transparent 151px, transparent 154px,
        #000 154px, #000 160px, transparent 160px, transparent 163px,
        #000 163px, #000 169px, transparent 169px, transparent 177px,
        #000 177px, #000 183px, transparent 183px, transparent 186px,
        #000 186px, #000 188px, transparent 188px, transparent 202px
    );
    animation: soundwave-drift 72s linear infinite;
    transition: filter 0.3s ease, animation-duration 0.3s ease;
}

.header-logo-container:hover::after {
    animation-duration: 10s;
    filter: drop-shadow(0 0 3px var(--accent-color));
}

/* Title & Section Dividers: Dual-track soundwave frequency pulse */
.post-title::after {
    height: 3px;
    border-bottom: none !important;
    background: repeating-linear-gradient(
        to right,
        var(--accent-color) 0px, var(--accent-color) 2px, transparent 2px, transparent 6px,
        var(--accent-color) 6px, var(--accent-color) 12px, transparent 12px, transparent 15px,
        var(--accent-color) 15px, var(--accent-color) 17px, transparent 17px, transparent 24px,
        var(--accent-color) 24px, var(--accent-color) 32px, transparent 32px, transparent 40px
    ) !important;
    animation: frequency-phase 48s linear infinite;
    opacity: 0.85;
    transition: opacity 0.3s ease, filter 0.3s ease;
}

.post-title:hover::after {
    opacity: 1;
    filter: drop-shadow(0 0 2px var(--accent-color));
}

/* On a post the rule is a cut, not a drawn line. Same Morse pattern, but used
   as a mask over a crop of the live field, so the dashes show the artwork
   through them exactly as the wordmark does — one device, two sizes. The
   accent-coloured version above still serves the excerpt titles. */
.post.content .post-title {
    border-bottom: none !important;
}

.post.content .post-title::after {
    content: none !important;
}

/* The drift is on the mask, so the dashes travel and the field stays put. */
@keyframes frequency-phase {
    0% {
        -webkit-mask-position: 0 0;
        mask-position: 0 0;
        background-position: 0 0;
    }
    100% {
        -webkit-mask-position: 120px 0;
        mask-position: 120px 0;
        background-position: 120px 0;
    }
}

/* The theme draws an <hr> across the post navigation and knocks a hole in it
   with a background on the heading. Both go: the divider is noise above two
   links, and the heading's fill was the one non-black surface on the page. */
.post-navigation-title hr {
    display: none;
}

.post-navigation-title h2 {
    background: transparent;
}

/* Interior header, matched to the home rail. These were bordered chips with a
   translucent fill and a lift-and-glow on hover — carried over from the old
   standalone landing page's buttons. The home surface replaced that language
   with plain links on an underline, and a visitor crossing from the home page
   to any other met four bordered boxes one click later. One language, not two.
   The Morse barcode beside the logo stays: it is a brand mark, not a box. */
.logo {
    /* The theme fills the logo with the accent colour and reverses the text out
       of it. That plate is the loudest object on an interior page and it has no
       counterpart on the home surface, where the aperture is the wordmark. */
    padding: 0;
    background: none;
    color: #f5f5f5;
    border-bottom: 1px solid transparent;
    transition: color 0.25s ease, border-color 0.25s ease;
}

.post-title a {
    color: #f5f5f5;
    text-decoration: none;
    transition: color 0.25s ease;
}

.main-menu-items a {
    display: inline-block;
    padding: 2px 0;
    color: rgba(245, 245, 245, 0.72);
    font-size: 0.82rem;
    letter-spacing: 0.06em;
    text-decoration: none;
    border-bottom: 1px solid transparent;
}

.post-title {
    color: #fff;
}

.logo:hover,
.main-menu-items a:hover,
.main-menu-items a:focus-visible {
    color: #fff;
    border-bottom-color: var(--accent-color);
}


/* ── The portrait ────────────────────────────────────────────────────────────

   The aperture at a second subject. A matte cut from the photograph's own
   luminance masks a field drawn live in the box behind it, so the face is made
   of the same filaments the wordmark is rather than photographed and set beside
   them. The shot's background was already black, which is why the matte is the
   picture's own tonality and not a shape approximating its edge.

   It animates by drifting the field under a fixed matte. Not by touching the
   filter: measured on a 6K display, mutating an SVG filter costs about 260ms a
   frame, and a transform costs the compositor nothing. The face holds still
   while the light inside it moves, which is the right way round anyway. */
.record-row {
    display: flex;
    align-items: stretch;
    gap: 40px;
    margin: 0 0 56px;
}

.record-row .record {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
}

.portrait {
    flex: 0 0 232px;
    position: relative;
    margin: 0;
    overflow: hidden;
    align-self: center;
    aspect-ratio: 640 / 788;
    /* The matte is on the fixed box, not on the layer that moves. Masking the
       drifting layer scales the mask with it and the face slides out of frame;
       this is the masthead's own model — the stage holds the shape, the field
       travels behind it. */
    -webkit-mask-image: url("portrait-matte.png");
    mask-image: url("portrait-matte.png");
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
}

.portrait-field {
    position: absolute;
    /* Oversized, so the drift has somewhere to travel without pulling an empty
       edge into the matte. */
    inset: -10%;
    animation: portrait-drift 62s ease-in-out infinite alternate;
    will-change: transform;
}

.portrait-field canvas {
    display: block;
    width: 100%;
    height: 100%;
}

/* Slow, and small: the point is that the light inside the face moves, not that
   the face does. */
@keyframes portrait-drift {
    0% {
        transform: translate3d(-1.5%, -1%, 0) scale(1.04);
    }
    100% {
        transform: translate3d(1.5%, 1.5%, 0) scale(1.1);
    }
}

@media (prefers-reduced-motion: reduce) {
    .portrait-field {
        animation: none;
    }
}

@media (max-width: 760px) {
    .record-row {
        flex-direction: column-reverse;
        gap: 28px;
    }

    .portrait {
        flex: 0 0 auto;
        width: 180px;
        align-self: flex-start;
    }
}

/* ── The reading column ──────────────────────────────────────────────────────

   A post is the one route that never had a wrapper of its own: its content is
   a direct child of <main>, so when the centred column came off <body> for the
   aperture, the prose went flush to x=0 and the title ran the full 1440 edge to
   edge. These two classes exist only on a post page, which is what makes them
   safe to anchor to.

   The type steps up with the measure. 16px was the theme's size for a column
   the theme centred for it; a page given over to reading can afford more, and
   the ramp scales with it rather than beside it — 32 / 23 / 18 keeps the same
   1.4 and 1.25 steps the system already uses, so a reader scanning for
   structure still finds it. The measure stays at 70ch: monospace wants shorter
   lines, and a bigger face does not change that, it just makes 70ch wider. */
.post.content,
.post-navigation {
    /* The cap is the measure, so the gutters sit outside it rather than eating
       into it — 70ch of text, not a 70ch box with 80px of padding inside. */
    max-width: calc(70ch + 80px);
    margin-inline: auto;
    padding-inline: 40px;
}

.post.content {
    font-size: 1.125rem;
    line-height: 1.62;
}


/* Blog list: rift window. Excerpts (article.post without .content) are stripped to
   title, one dim meta line, description. On hover the article's backdrop is brightened,
   lifting body::before's 82% black so the rift glows through behind the post. */
.post:not(.content) {
    padding: 30px 20px;
    margin-inline: -20px;
    border-radius: 2px;
    backdrop-filter: brightness(1) saturate(1);
    transition: backdrop-filter 0.6s ease;
}

.post:not(.content):hover {
    backdrop-filter: brightness(2.6) saturate(1.3);
}

.post:not(.content) .post-title {
    padding-bottom: 0;
    margin-bottom: 8px;
    border-bottom: none;
}

.post:not(.content) .post-title::after,
.post:not(.content) .post-tags,
.post:not(.content) .read-more,
.post:not(.content) li[title="Author"],
.post:not(.content) li[title="Author"] + li {
    display: none;
}

.post:not(.content) .post-title a {
    border: none;
    background: none;
    padding: 0;
    box-shadow: none;
    transform: none;
}

.post:not(.content) ul.post-meta {
    font-size: 0.85em;
    color: color-mix(in srgb, var(--text-color) 68%, transparent);
}

.post:not(.content) p {
    margin-bottom: 0;
    color: color-mix(in srgb, var(--text-color) 70%, transparent);
}

/* Blog list: spectrum spine. 2px rule on each excerpt's left edge; --hue is set per
   post in base.html (index across the wheel), same idea as the rift laying the full
   spectrum down the viewport. Glows on hover. */
.post:not(.content) {
    position: relative;
}

.post:not(.content)::before {
    content: "";
    position: absolute;
    top: 30px;
    bottom: 30px;
    left: 0;
    width: 2px;
    background: hsl(var(--hue, 300) 100% 72%);
    opacity: 0.85;
    transition: opacity 0.6s ease, filter 0.6s ease;
}

.post:not(.content):hover::before {
    opacity: 1;
    filter: drop-shadow(0 0 4px hsl(var(--hue, 300) 100% 72%));
}

/* Blog list titles. These were hollow — transparent fill over a 1px 55%-white
   stroke — which made the page's primary navigation targets its least legible
   element, and collided with itself on two-line titles because the theme's
   global line-height is shorter than this font size. The aperture on the home
   page is where a hollow letterform earns its keep: there the rift is genuinely
   behind the glyph. Here there is nothing behind it but the veil, so the effect
   was paying a readability cost for no picture. Solid, with the per-post hue on
   hover carrying the connection to the spine instead. */
.post:not(.content) .post-title {
    font-size: 1.6em;
    line-height: 1.3;
}

.post:not(.content) .post-title a {
    color: #f5f5f5;
    transition: color 0.3s ease;
}

.post:not(.content):hover .post-title a,
.post:not(.content):focus-within .post-title a {
    color: hsl(var(--hue, 300) 100% 72%);
}

/* Reading measure. The column was 82ch of monospace; mono wants shorter lines
   than a proportional face, not longer. Headings and figures keep the full
   column so the narrowing reads as a measure, not a second column. */
.post.content > p,
.post.content > ul,
.post.content > ol,
.post.content > blockquote {
    max-width: 70ch;
}

/* Heading scale on a post. The theme sets h1 at 1.45× and h2 at 1.35× of body,
   which is a 1.07 step between a post's own title and its section headings —
   flat enough that a reader scanning for structure finds none. Both also
   inherited the global line-height of 24.64px, shorter than the type itself, so
   any heading that wrapped collided with its own second line.

   32 / 23 / 18px, steps of 1.4 and 1.25. The ramp moved up with the body text
   when the reading column grew; the steps are the same, so a reader scanning
   for structure still finds it. h3 sits at body size and is separated by weight
   rather than scale: a fourth size between 23 and 18 would be a step of 1.1,
   which is the flatness this rule exists to remove. Each heading gets more room
   above it than below, so it binds to the text it introduces. */
.post.content .post-title {
    font-size: 2rem;
    line-height: 1.22;
}

.post.content h2 {
    font-size: 1.4rem;
    line-height: 1.3;
    margin-top: 2.4em;
    margin-bottom: 0.65em;
}

.post.content h3 {
    font-size: 1.125rem;
    line-height: 1.4;
    margin-top: 2em;
    margin-bottom: 0.6em;
}

@media (max-width: 760px) {
    .post.content,
    .post-navigation {
        padding-inline: 20px;
    }

    /* The display step does not survive a 350px column: 32px of monospace puts
       this post's title on six lines. The body text stays where it is — the
       point was a bigger reading size, and that is unaffected by the heading. */
    .post.content .post-title {
        font-size: 1.6rem;
        line-height: 1.28;
    }

    .post.content h2 {
        font-size: 1.25rem;
    }
}


/* The two decorative loops above — the header barcode's drifting spectrum and the
   title rule's frequency pulse — ran unconditionally. rift.js and rift.css both
   honour this preference, so these were the only motion on the site that did not.
   They keep their painted appearance; they simply stop moving. */
@media (prefers-reduced-motion: reduce) {
    .header-logo-container::after,
    .header-logo-container:hover::after,
    .post-title::after {
        animation: none;
    }
}

/* ── The manual frame: /about and /resume ─────────────────────────────────────

   Read surfaces. The world owns the frame — the aperture masthead, the rail,
   the section labels hung in the margin, the way the reference tables are set —
   and the reading column stays calm: plate black, one measure, nothing moving
   behind the words.

   Same ancestor rule as the home page: nothing above the masthead SVG in the
   tree may carry a background, or it paints under the mask and fills the cut.
   So <body> and <main> stay transparent and each region paints its own black. */

/* The rail. Same language as the home page: plain links on an underline. */
.manual {
    --label: 150px;
    --gutter: 30px;
    max-width: 1020px;
    margin-inline: auto;
    padding: 0 40px 72px;
}

/* The record. Five fields, hairline-ruled, the thirty-second read. */
.record {
    display: flex;
    flex-wrap: wrap;
    gap: 0;
    margin: 0 0 56px;
    border-top: 1px solid rgba(245, 245, 245, 0.1);
    border-bottom: 1px solid rgba(245, 245, 245, 0.1);
}

.record > div {
    flex: 1 1 auto;
    min-width: 0;
    padding: 16px 28px 18px 0;
}

.record > div + div {
    padding-left: 28px;
    border-left: 1px solid rgba(245, 245, 245, 0.1);
}

.record dt {
    margin: 0 0 6px;
    font-size: 0.74rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: rgba(245, 245, 245, 0.5);
}

.record dd {
    margin: 0;
    font-size: 0.92rem;
    color: #f5f5f5;
}

.record dd a {
    color: rgba(245, 245, 245, 0.72);
    text-decoration: none;
    border-bottom: 1px solid rgba(245, 245, 245, 0.25);
    margin-right: 12px;
    transition: color 0.25s ease, border-color 0.25s ease;
}

.record dd a:hover,
.record dd a:focus-visible {
    color: #fff;
    border-bottom-color: var(--accent-color);
}

/* Section labels hang in the gutter. The float sits entirely inside the
   column's padding, so it never narrows the measure beside it. */
.manual-section {
    margin-bottom: 64px;
}

/* The gutter the hung labels used to occupy is gone with them; the body keeps
   its own measure and the section heads rule the full column above it. */
.manual-body {
    padding-left: 0;
    max-width: 68ch;
}

/* Section headings wear the ledger row's language: a hairline across the
   column, a chip cropped from the field, then the label. They used to hang in
   the left gutter as small uppercase labels, which worked but shared nothing
   with the lists. One object now marks a section anywhere on the site.

   The chip is landscape here rather than portrait. The tear runs horizontally,
   so a wide short crop shows the seam travelling through it — the same reading
   as the masthead, at list scale. */
.manual-label,
.manual-prose h2 {
    position: relative;
    display: flex;
    align-items: center;
    gap: 18px;
    float: none;
    width: auto;
    margin: 0 0 20px;
    padding-top: 14px;
    border: none;
    font-size: 0.78rem;
    font-weight: 400;
    line-height: 1.4;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: rgba(245, 245, 245, 0.62);
}

/* The rule takes the section's own hue and lets go of it across the column,
   so the head is lit from the same source as its chip rather than ruled with a
   neutral hairline that belongs to nothing. */
.manual-label::before,
.manual-prose h2::before {
    content: "";
    position: absolute;
    inset: 0 0 auto;
    height: 1px;
    background: linear-gradient(
        to right,
        hsl(var(--hue, 300) 100% 72% / 0.75),
        rgba(245, 245, 245, 0.1) 34%,
        rgba(245, 245, 245, 0.1)
    );
}

/* Pushed to the far edge, the way a ledger row's category and read-time are.
   It is what the section contains, counted, not a label repeated. */
.section-meta {
    margin-left: auto;
    font-size: 0.78rem;
    letter-spacing: 0.06em;
    text-transform: none;
    font-variant-numeric: tabular-nums;
    color: rgba(245, 245, 245, 0.5);
}

.manual-prose h2 {
    margin-top: 46px;
}


.manual-prose h2 a {
    display: none;
}

.manual-body > p,
.manual-prose p {
    margin: 0 0 1.1em;
    line-height: 1.62;
    color: #f5f5f5;
}

.manual-close {
    color: rgba(245, 245, 245, 0.72);
}

.manual-note {
    font-size: 0.82rem;
    color: rgba(245, 245, 245, 0.5);
    border-left: 1px solid rgba(245, 245, 245, 0.18);
    padding-left: 14px;
    margin-bottom: 2.2em;
}

.manual-prose ul {
    margin: 0 0 1.4em;
    padding-left: 1.1em;
}

.manual-prose li {
    margin-bottom: 0.5em;
    line-height: 1.6;
}

/* A role. The organisation leads, the title answers it, the dates sit quiet
   underneath — they carry the overlap, so they stay exact and legible. */
.role {
    padding-top: 26px;
    margin-bottom: 30px;
    border-top: 1px solid rgba(245, 245, 245, 0.1);
}

.role-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 16px;
    margin: 0 0 4px;
    font-size: 1rem;
    line-height: 1.4;
    border-bottom: none;
    padding-bottom: 0;
}

.role-org {
    font-weight: 700;
    color: #f5f5f5;
}

.role-title {
    font-weight: 400;
    color: rgba(245, 245, 245, 0.72);
}

.role-when {
    display: flex;
    gap: 16px;
    margin: 0 0 12px;
    font-size: 0.78rem;
    color: rgba(245, 245, 245, 0.5);
}

.role-dates {
    font-variant-numeric: tabular-nums;
}

.role-context {
    color: rgba(245, 245, 245, 0.82);
}

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

.role-points li {
    position: relative;
    padding-left: 20px;
    margin-bottom: 0.7em;
    font-size: 0.92rem;
    line-height: 1.62;
    color: rgba(245, 245, 245, 0.78);
}

.role-points li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.72em;
    width: 10px;
    height: 1px;
    background: rgba(245, 245, 245, 0.35);
}

/* Evidence, pairs and credits: the same ruled-row grammar three times. */
.evidence,
.credits {
    margin: 0;
    padding: 0;
    list-style: none;
}

.evidence li,
.credits li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 20px;
    padding: 14px 0;
    border-top: 1px solid rgba(245, 245, 245, 0.1);
}

.evidence li:last-child,
.credits li:last-child {
    border-bottom: 1px solid rgba(245, 245, 245, 0.1);
}

.evidence li {
    position: relative;
    padding-left: 18px;
}

.evidence li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 18px;
    width: 2px;
    height: 14px;
    background: hsl(var(--hue, 300) 100% 72%);
    opacity: 0.85;
}

.ev-name {
    flex: none;
    width: 17ch;
    color: #f5f5f5;
}

.cr-what {
    flex: none;
    width: 30ch;
    color: #f5f5f5;
}

.ev-kind {
    flex: none;
    width: 17ch;
    font-size: 0.72rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    white-space: nowrap;
    color: rgba(245, 245, 245, 0.5);
}

.ev-what,
.cr-who {
    flex: 1 1 22ch;
    font-size: 0.92rem;
    color: rgba(245, 245, 245, 0.72);
}

.ev-ref,
.cr-year {
    flex: none;
    margin-left: auto;
    font-size: 0.8rem;
    color: rgba(245, 245, 245, 0.5);
    font-variant-numeric: tabular-nums;
}

.ev-ref {
    text-decoration: none;
    border-bottom: 1px solid transparent;
    transition: color 0.25s ease, border-color 0.25s ease;
}

.ev-ref:hover,
.ev-ref:focus-visible {
    color: #fff;
    border-bottom-color: var(--accent-color);
}

.pairs {
    margin: 0;
}

.pairs > div {
    display: flex;
    gap: 20px;
    padding: 12px 0;
    border-top: 1px solid rgba(245, 245, 245, 0.1);
}

.pairs > div:last-child {
    border-bottom: 1px solid rgba(245, 245, 245, 0.1);
}

.pairs dt {
    flex: none;
    width: 17ch;
    font-size: 0.72rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    white-space: nowrap;
    color: rgba(245, 245, 245, 0.5);
    padding-top: 0.2em;
}

.pairs dd {
    margin: 0;
    color: rgba(245, 245, 245, 0.88);
}

/* The cross-reference index. The densest block on the page, and the one that
   makes the route feel like part of a site rather than a leaf. */
.manual-index-section .manual-body {
    max-width: none;
}

.manual-index {
    margin: 0;
    padding: 0;
    list-style: none;
    columns: 3;
    column-gap: 40px;
}

.manual-index li {
    break-inside: avoid;
    position: relative;
    padding-left: 14px;
}

.manual-index li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.62em;
    width: 2px;
    height: 11px;
    background: hsl(var(--hue, 300) 100% 72%);
    opacity: 0.85;
}

.manual-index a {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 5px 0;
    text-decoration: none;
    color: rgba(245, 245, 245, 0.82);
    transition: color 0.25s ease;
}

.manual-index a:hover,
.manual-index a:focus-visible {
    color: #fff;
}

.idx-name {
    font-size: 0.86rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.idx-kind {
    flex: none;
    margin-left: auto;
    font-size: 0.74rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: rgba(245, 245, 245, 0.5);
}

@media (max-width: 900px) {
    .manual-index {
        columns: 2;
    }
}

@media (max-width: 760px) {
    .manual {
        padding-inline: 20px;
    }

    .manual {
        --label: 0px;
        --gutter: 0px;
    }

    .manual-label,
    .manual-prose h2 {
        float: none;
        width: auto;
        margin: 0 0 10px;
        padding-top: 0;
    }

    .record > div + div {
        padding-left: 0;
        border-left: none;
    }

    .record > div {
        flex: 1 1 100%;
        padding: 12px 0;
        border-top: 1px solid rgba(245, 245, 245, 0.1);
    }

    .record > div:first-child {
        border-top: none;
    }

    .manual-index {
        columns: 1;
    }

    .ev-name,
    .ev-kind,
    .cr-what {
        width: auto;
    }

    .ev-ref,
    .cr-year {
        margin-left: 0;
    }
}

/* A resume gets printed. Drop the frame, keep the record and the structure. */
@media print {
    .site-rail,
    .aperture-link,
    .grain-overlay,
    #canvas-container,
    .footer,
    .manual-index-section {
        display: none !important;
    }

    html,
    body,
    .manual {
        background: #fff !important;
        color: #000 !important;
    }

    .manual-body > p,
    .record dd,
    .role-org,
    .role-context,
    .role-points li,
    .ev-name,
    .ev-what,
    .cr-what,
    .pairs dd {
        color: #000 !important;
    }

    /* Print density. The web layout hangs its labels in a 180px gutter and holds
       a 68ch measure; carried straight to paper that runs to eight pages. On
       paper the label goes inline above its section and the column takes the
       full page width, which is what the format wants anyway. */
    .manual {
        padding: 0;
    }

    .manual-section {
        margin-bottom: 18pt;
    }

    .manual-body {
        padding-left: 0;
        max-width: none;
    }

    .manual-label,
    .manual-prose h2 {
        float: none;
        width: auto;
        margin: 0 0 4pt;
        padding-top: 0;
        font-size: 8pt;
    }

    .manual-body > p,
    .manual-prose p,
    .role-context {
        font-size: 9.5pt;
        line-height: 1.38;
        margin-bottom: 6pt;
    }

    .role {
        break-inside: avoid;
        padding-top: 10pt;
        margin-bottom: 10pt;
    }

    .role-head {
        font-size: 10.5pt;
        margin-bottom: 2pt;
    }

    .role-when {
        font-size: 8pt;
        margin-bottom: 5pt;
    }

    .role-points li {
        font-size: 9pt;
        line-height: 1.36;
        margin-bottom: 3pt;
    }

    .record > div {
        padding: 6pt 14pt 6pt 0;
    }

    .evidence li,
    .credits li,
    .pairs > div {
        padding: 5pt 0;
        break-inside: avoid;
    }
}

/* The reading column sits left so the hung labels have a gutter, which leaves a
   wide right margin at desktop widths. Capping the frame keeps that margin from
   reading as half an empty page. */
/* The block stays full-bleed so it paints its own black edge to edge; only the
   content inside it is capped. Centring the block itself with `margin: auto`
   leaves its flanks transparent, and the raw field bleeds in beside the text. */
/* <main> may carry a background: the masthead SVG lives in <header>, so nothing
   here paints under the mask. <header> itself must stay transparent. */
/* ── The rail frame: /blog, /tags, /categories and every term page ───────────

   The artwork is a left-edge phenomenon. rift.js starts every line in the
   leftmost tenth of the canvas and drifts it only a few hundred pixels right,
   so the brightness and the whole hue wheel live in roughly the left fifth and
   the rest falls to near-black. The old layout veiled that fifth at 82% and
   cropped what survived to a hard strip with no intent behind it.

   Here the field simply keeps that column. Nothing paints over it: <body> is
   padded by the rail width and every region inside the padding paints its own
   black, which also keeps the masthead's cut working — a background on an
   ancestor of that SVG would fill the hole. The seam is a hard cut with a
   hue-graded hairline, the same gesture as the masked glyphs. */

/* Sparse routes have less to say, so the field gets more of the page. */
/* The seam. A hairline that takes its colour from the field it is cutting. */
.page {
    padding: 0 40px 72px;
    max-width: 1020px;
    margin-inline: auto;
}

.page-head {
    padding: 8px 0 34px;
}

.page-head h1 {
    display: flex;
    align-items: center;
    gap: 16px;
    margin: 0 0 6px;
    font-size: 1.75rem;
    line-height: 1.25;
    border-bottom: none;
    padding-bottom: 0;
}

.page-count {
    margin: 0;
    font-size: 0.78rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: rgba(245, 245, 245, 0.5);
}

/* ── Swatches ────────────────────────────────────────────────────────────────

   Each one is a crop of the live canvas taken at the item's own hue, painted by
   swatch.js. The background below is the fallback and the resting colour before
   the field finishes drawing, so a chip is never an empty box. Chips are kept
   at 24x72 or larger: measured smaller, the field's structure collapses and the
   chip is just a colour block, which the 2px rule it replaced already did. */

.swatch {
    position: relative;
    flex: none;
    display: block;
    width: 24px;
    height: 72px;
    overflow: hidden;
    /* Neutral until something knows this item's hue. A coloured default would
       make every chip on a page the same colour before the field finishes
       drawing, and permanently with scripting off — which is worse than no
       colour, because it looks like a signal and carries none. */
    background: rgba(245, 245, 245, 0.08);
    box-shadow: inset 0 0 0 1px rgba(245, 245, 245, 0.14);
    transition: box-shadow 0.3s ease, filter 0.3s ease;
}

/* The template sets --hue where it can work it out; swatch.js sets it for the
   rest as it paints. */
.swatch[style*="--hue"],
.swatch.is-painted {
    background: hsl(var(--hue, 300) 100% 40%);
}

.swatch canvas {
    display: block;
    width: 100%;
    height: 100%;
}

/* Landscape, because the tear is horizontal: a wide short crop shows the seam
   travelling through the chip, which is the masthead's own reading at list
   scale. Declared after .swatch so it wins on source order, not specificity. */
.swatch-head {
    width: 72px;
    height: 18px;
}

/* The rule under a post title: the Morse pattern as a mask over a crop of the
   field, so the dashes show the artwork through them the way the wordmark
   does. Declared after .swatch so it wins on source order — the base class
   sets 24x72 and a media query would not change that. */
.rule-swatch {
    --morse: repeating-linear-gradient(
        to right,
        #000 0px, #000 2px, transparent 2px, transparent 6px,
        #000 6px, #000 12px, transparent 12px, transparent 15px,
        #000 15px, #000 17px, transparent 17px, transparent 24px,
        #000 24px, #000 32px, transparent 32px, transparent 40px
    );
    display: block;
    width: 100%;
    height: 6px;
    margin: 14px 0 18px;
    background: none;
    box-shadow: none;
    -webkit-mask-image: var(--morse);
    mask-image: var(--morse);
    -webkit-mask-repeat: repeat-x;
    mask-repeat: repeat-x;
    animation: frequency-phase 48s linear infinite;
}


.swatch-lg {
    width: 34px;
    height: 96px;
}

/* The ledger in a rail page: the swatch leads the row. */
.ledger-rail {
    --ledger-date: 0px;
}

.ledger-rail .ledger-row::before {
    content: none;
}

/* No wrap here. The manual frame lets rows wrap so a summary can take its own
   line; in the rail the summary lives inside .ledger-main, and with wrapping on
   an oversized item jumps to a new line instead of shrinking, which throws a
   long title under its own date. */
.ledger-rail .ledger-row a {
    flex-wrap: nowrap;
    align-items: center;
    gap: 0 24px;
    padding: 16px 0;
}

.ledger-rail .ledger-row time {
    width: 11ch;
}

.ledger-rail .ledger-main {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.ledger-rail .ledger-summary {
    flex: none;
    padding: 0;
    white-space: normal;
}

/* The title wraps rather than truncating. The row does not wrap, so the main
   column shrinks and the title takes a second line inside it — a clipped title
   in a list the reader is scanning is worse than an uneven row height. */
.ledger-rail .ledger-title {
    flex: none;
    white-space: normal;
    line-height: 1.35;
}

.ledger-rail .ledger-tail {
    flex: none;
}

.ledger-rail .ledger-row:hover .swatch,
.ledger-rail .ledger-row:focus-within .swatch {
    box-shadow: inset 0 0 0 1px rgba(245, 245, 245, 0.4);
    filter: brightness(1.35) saturate(1.1);
}

/* ── The term index ─────────────────────────────────────────────────────────
   Twenty-one tags, each keeping the colour its own name gives it, so the index
   reads as the field sampled rather than a sorted rainbow. */

.term-index {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 2px 40px;
}

.term-index a {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 10px 0;
    border-top: 1px solid rgba(245, 245, 245, 0.1);
    text-decoration: none;
    color: #f5f5f5;
    transition: color 0.25s ease;
}

.term-index .swatch {
    width: 24px;
    height: 48px;
}

.term-name {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 0.95rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.term-count {
    flex: none;
    font-size: 0.8rem;
    color: rgba(245, 245, 245, 0.5);
    font-variant-numeric: tabular-nums;
}

.term-index a:hover,
.term-index a:focus-visible {
    color: hsl(var(--hue, 300) 100% 78%);
}

.term-index a:hover .swatch,
.term-index a:focus-visible .swatch {
    box-shadow: inset 0 0 0 1px rgba(245, 245, 245, 0.4);
    filter: brightness(1.35) saturate(1.1);
}

.pagination ul {
    display: flex;
    gap: 28px;
    margin: 40px 0 0;
    padding: 0;
    list-style: none;
}

.pagination a {
    font-size: 0.82rem;
    letter-spacing: 0.06em;
    color: rgba(245, 245, 245, 0.72);
    text-decoration: none;
    border-bottom: 1px solid transparent;
}

.pagination a:hover,
.pagination a:focus-visible {
    color: #fff;
    border-bottom-color: var(--accent-color);
}

@media (max-width: 860px) {
    .page {
        padding-inline: 20px;
    }

    .ledger-rail .ledger-row a {
        flex-wrap: wrap;
        gap: 6px 16px;
    }

    .ledger-rail .ledger-main {
        flex-basis: 100%;
        order: 3;
    }
}
