/* ==========================================================================
   SINEMILL — components ("berlin")
   The stenographer, the plane shells, bands, buy, spec lists, the colophon,
   the explore bar, the info block and the grain.

   Deliberately absent, and not oversights:
     · marquees — a scrolling ticker reads as a stock website device
     · section markers, fig. numbers, spec letters — the whole lab-report
       numbering layer. Scale, weight and position carry the hierarchy.
     · an index/summary table — the page has just shown you the products; a
       summary immediately after is a weaker second telling.
     · any permanent decorative text. Every word on a page has to be real,
       editable content, or nobody can ever change it.
   ========================================================================== */

/* ==========================================================================
   1. THE STENOGRAPHER
   --------------------------------------------------------------------------
   Three modes via data-steno:

     line   the trailing space lives INSIDE the accent box, so consecutive
            words fuse into a continuous bar and each line ends ragged.
            Headlines.
     box    a discrete box per word. Captions.
     plain  no box, reveal only — and paced roughly four times faster,
            because a paragraph transcribed at headline speed is a paragraph
            nobody waits for.

   The mistake is THREE states, not two: the wrong word lands, holds,
   VANISHES completely, and the right word appears in the gap it left. The
   blank frame is the whole trick — a same-place swap has no motion for the
   eye to catch.

   Layout is final from frame zero: hidden words use `visibility: hidden`, so
   nothing reflows as the text is written. The real text is always in the DOM;
   the wrong word is an out-of-flow overlay on top of a word that is already
   correct underneath, so crawlers and screen readers never see a typo and a
   longer wrong word can never move the line.
   ========================================================================== */

.steno { --sw-box: var(--accent); --sw-on: var(--accent-on); }

.sw {
    position: relative;
    display: inline-block;
    visibility: hidden;
}
.sw.is-in { visibility: visible; }

/* THE NO-JAVASCRIPT FLOOR UNDER fit.js. In `line` mode below, a word is an
   inline-block with `white-space: pre` — atomic, unwrappable, and wider than
   its column whenever the copy contains a long word. fit.js answers that by
   setting the headline smaller, which is the designed behaviour; this line is
   what happens when fit.js never runs, and all it has to do is keep the word
   on the screen. It is inert while the stenographer's spans exist, because
   `white-space: pre` forbids the break it permits — so it costs nothing and
   changes nothing on the normal path. */
.steno { overflow-wrap: break-word; }

/* THE LEADING IS LOAD-BEARING — and it is a measured value, not a taste
   call. See --lh-knockout in tokens.css for the measurement and why the
   overlap direction is deliberate. A new display face means re-measuring. */
.steno[data-steno="line"] { line-height: var(--lh-knockout); }
.steno[data-steno="line"] .sw { white-space: pre; }

.sw-t {
    display: inline;
    -webkit-box-decoration-break: clone;
            box-decoration-break: clone;
}

.steno[data-steno="line"] .sw-t,
.steno[data-steno="box"]  .sw-t {
    background: var(--sw-box);
    color: var(--sw-on);
    padding: 0.02em 0.06em 0.06em;
}
.steno[data-steno="box"] .sw-t { padding-left: 0.22em; padding-right: 0.22em; }

.steno[data-tone="paper"] { --sw-box: var(--paper); --sw-on: var(--ink); }
.steno[data-tone="cyan"]  { --sw-box: var(--cyan); }
.steno[data-tone="lime"]  { --sw-box: var(--lime); }
.steno[data-tone="ink"]   { --sw-box: var(--ink); }

/* ---- The mistake -------------------------------------------------------- */
.sw-typo {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 2;
    white-space: pre;
    pointer-events: none;
    background: var(--sw-box);
    color: var(--sw-on);
    padding: 0.02em 0.06em 0.06em;
    /* Its parent .sw is still hidden while this is on screen — during the
       wrong-word phase the correct word must not be visible. `visibility` is
       inherited but overridable, which is what lets one word be wrong
       without duplicating or rewriting any real text. */
    visibility: visible;
}
.steno[data-steno="box"] .sw-typo { padding-left: 0.22em; padding-right: 0.22em; }
.steno[data-steno="plain"] .sw-typo { background: none; color: inherit; padding: 0; }

/* ---- The caret ---------------------------------------------------------- */
.sw.is-live::after {
    content: "";
    position: absolute;
    right: -0.42em;
    top: 0.08em;
    width: 0.3em;
    height: 0.84em;
    background: var(--paper);
    animation: steno-caret 0.22s steps(1, end) infinite;
}
.steno[data-steno="plain"] .sw.is-live::after { right: -0.34em; }

/* While a word is being backspaced the caret has to sit at the end of the
   SHRINKING text, not at the end of the finished word — otherwise it hovers
   in empty space and the deletion stops reading as deletion. */
.sw.is-fixing.is-live::after { display: none; }
.sw-typo::after {
    content: "";
    position: absolute;
    left: 100%;
    top: 0.1em;
    width: 0.3em;
    height: 0.8em;
    background: var(--paper);
    animation: steno-caret 0.22s steps(1, end) infinite;
}

@keyframes steno-caret {
    0%, 49%   { opacity: 1; }
    50%, 100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    .sw { visibility: visible; }
    .sw.is-live::after { display: none; }
}

/* ==========================================================================
   2. PLANES
   --------------------------------------------------------------------------
   NOTHING IS EVER CROPPED. Each plane takes its media's own aspect — the
   media sits in normal flow at width:100% and the box is whatever height
   that makes. Size is controlled purely by how many columns the plane spans.

   That is a real constraint on the layout rather than a default: a plugin
   window that is 810×1440 makes any plane showing it a tall portrait object,
   and the composition has to want one. The alternative was cutting through
   the plugin's own controls, which made every screenshot look broken.

   TWO canvases, not one. A single shared canvas can only sit at one z-index,
   so a plane could go behind text or in front of it but the page could not
   have both — and z-index on the <figure> does nothing, because the figure
   is invisible and the renderer draws it. A plane marked z-index:5 to sit
   above a headline rendered BELOW it, and the headline's knockout box ate a
   hole in the image. Hence .gl--back / .gl--front, with text between them.

   PLANE SIZE IS MOTION AMPLITUDE. Bend is uBend × sin(v) × uSize.y × 0.42,
   so shrinking a plane shrinks how much it moves — halving a hero's height
   halved its peak bend and the whole page read as flatter. If a plane must
   shrink, raise its data-bend.
   ========================================================================== */

.plane {
    position: relative;
    display: block;
    margin: 0;
}

.plane__media {
    display: block;
    width: 100%;
    height: auto;
    transition: opacity 0.2s linear;
}

/* A <video> with no width/height attributes has an INTRINSIC SIZE OF 300x150
   until its metadata loads — and these are preload="none", so for most of
   them it never does. Every such plane collapsed to 150px tall regardless of
   how many columns it spanned, which turned a feature demo into a letterbox
   strip.

   `aspect-ratio: auto 16 / 10` is the exact tool for this: the `auto` keyword
   means a real intrinsic ratio still wins when one is available, and the
   ratio is only the fallback. Setting a bare `aspect-ratio: 16 / 10` would
   override the true dimensions on every video that DOES report them, which is
   worse than the bug.

   PHP emits width/height from the poster wherever the library knows them; this
   covers the rest, including feature media stored as bare URLs that never
   resolve back to an attachment. */
video.plane__media { aspect-ratio: auto 16 / 10; }

/* .is-drawn is added by planes.js PER PLANE, once that plane's own texture is
   actually on the GPU — not when the renderer starts. Keying on `html.gl-on`
   alone hid every fallback on the assumption that starting meant drawing,
   and a plane whose media never decodes (an undecodable codec, a 404) then
   rendered as a hole: a labelled empty box where a screenshot should be.
   Strictly worse than no renderer at all, since the real <img> was right
   there and had been hidden on spec. */
html.gl-on .plane.is-drawn .plane__media { opacity: 0; }

/* Without GL the planes still need to feel angled — flat but violently set.
   The angle is written by planes.js from data-tilt-y so there is one source. */
html:not(.gl-on) .plane[data-tilt-y] {
    transform: rotate(var(--fallback-rot, 0deg));
}

/* A ROTATED BOX IS WIDER THAN THE BOX.

   Its axis-aligned bounding box grows by `height x sin(angle)`, and scrollable
   overflow is computed from the TRANSFORMED box — so a plane sitting exactly
   inside its max-width still pushed the document 7-10px wider than the
   viewport below about 960px, and the page grew a horizontal scrollbar. Only
   on the no-WebGL path, and only when the viewport is narrow: which is to say
   on older hardware, and at 175% zoom and above. Both of those describe
   someone who is already having a harder time than everyone else.

   The angle is damped rather than removed — the fallback exists to make a
   plane feel set rather than pasted, and a flat rectangle is not that. The
   factor is measured, not guessed: 0.35 is the largest value that leaves zero
   overflow at 960, 823, 720, 560 and 390. */
@media (max-width: 960px) {
    html:not(.gl-on) .plane[data-tilt-y] {
        transform: rotate(calc(var(--fallback-rot, 0deg) * 0.35));
    }
}

.plane--link { cursor: pointer; }

/* Per-plane demo audio. Wired to the product's own showcase renders; hidden
   entirely when that product has none, rather than rendering a dead control. */
.plane__audio {
    position: absolute;
    right: 0;
    top: 100%;
    margin-top: 0.55em;
    z-index: 7;
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    font-family: var(--f-pixel);
    font-size: var(--t-nano);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink);
    background: var(--paper);
    padding: 0.35em 0.75em;
}
.plane__audio[aria-pressed="true"] { background: var(--cyan); }
.plane__audio[disabled] {
    background: none;
    color: var(--paper-faint);
    border: var(--rule-w) solid var(--rule-faint);
    cursor: not-allowed;
}

.gl {
    position: fixed;
    inset: 0;
    pointer-events: none;
    display: block;
    width: 100%;
    height: 100%;
}
.gl--back  { z-index: 1; }   /* text at z-index 3+ paints over these */
.gl--front { z-index: 5; }   /* these paint over the text */
html:not(.gl-on) .gl { display: none; }

/* ==========================================================================
   3. BANDS — full-bleed interruptions
   ========================================================================== */

.band {
    position: relative;
    z-index: 3;
    background: var(--accent);
    color: var(--accent-on);
    padding: var(--s-24) var(--gutter);
    margin: var(--s-32) calc(var(--gutter) * -1);
}
.band--ink   { background: var(--ink); color: var(--paper); }
.band--paper { background: var(--paper); color: var(--ink); }
.band .steno + p { margin-top: var(--s-12); }

/* WHAT CHANGED. The band was a left-aligned column: a blackletter line broken
   over three or four lines at 15ch, a mono note under it, and then two thirds
   of a full-bleed accent panel left empty. At the width this thing spans that
   read as an unfinished page rather than an interruption — the point of a
   full-bleed band is that it FILLS, and it was a tall coloured margin.

   It is now a spread. The line runs across the panel at up to 22ch, so it
   breaks in two or three big pieces rather than four narrow ones, and the note
   sits opposite it on the baseline rather than beneath it. A rule between them
   does the work the empty space was failing to do. */
.band {
    display: grid;
    grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
    align-items: end;
    gap: var(--s-12) var(--gutter);
    padding: var(--s-32) var(--gutter) var(--s-24);
}

/* The one blackletter moment. Pirata One is unreadable in quantity and stops
   being a jolt the second time you see it, so it gets exactly one line per
   page and --f-black appears nowhere else in the system. */
.band__shout {
    font-family: var(--f-black);
    font-size: clamp(2.6rem, 1rem + 8.5vw, 10rem);
    line-height: 0.92;
    max-width: 22ch;
    margin: 0;
}

.band__note {
    margin: 0 0 0.6em;
    max-width: 46ch;
    padding-top: var(--s-8);
    border-top: 1px solid currentColor;
    font-family: var(--f-mono);
    font-size: var(--t-label);
    line-height: 1.5;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: inherit;
}

@media (max-width: 860px) {
    .band { grid-template-columns: minmax(0, 1fr); }
    .band__note { margin-top: var(--s-8); }
}
.band--ink .band__note { color: var(--paper-dim); }

/* ==========================================================================
   4. BUY
   --------------------------------------------------------------------------
   Two of them. The band in the flow is the real one; the sticky bar is a
   thin repeat that follows from the moment you scroll past the hero, so on a
   page this long the price and the button are never more than a glance away.

   Free-flowing is a claim about layout, not about whether someone can find
   the button.
   ========================================================================== */

/* A <form>, not an <a>. Adding to a cart changes state, and a GET URL that
   changes state gets prefetched by browsers and followed by crawlers — which
   silently fills carts nobody asked to fill. The whole band is the accent
   ground; the <button> inside it is transparent and fills it, so the click
   target is the band while the accessible control is a real submit button. */
.buy {
    position: relative;
    z-index: 6;
    display: block;
    background: var(--accent);
    color: var(--accent-on);
    margin: var(--s-32) calc(var(--gutter) * -1);
    padding: var(--s-16) var(--gutter);
    transition: background var(--d-snap) var(--e-hard);
}
.buy:hover           { background: var(--accent-hot); }
.buy:active          { background: var(--accent-deep); }
/* focus-within, not focus: the focus lands on the button inside, and without
   this the band gives no feedback at all to a keyboard user. */
.buy:focus-within    { background: var(--accent-hot); }

button.buy__row {
    /* Overrides base.css's global button rules, which would otherwise paint
       an accent-on-accent block inside the band. */
    width: 100%;
    background: none !important;
    color: inherit !important;
    border: 0 !important;
    padding: 0 !important;
    min-height: 0;
    text-align: left;
    font: inherit;
    letter-spacing: inherit;
    text-transform: none;
}

.buy__row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-8);
    flex-wrap: wrap;
}
.buy__word,
.buy__price {
    font-family: var(--f-grotesk);
    font-variation-settings: 'wdth' 68, 'wght' 900;
    font-size: clamp(3.5rem, 14vw, 13rem);
    line-height: 0.78;
    letter-spacing: -0.03em;
    text-transform: uppercase;
}
.buy__price sup,
.buy__price .woocommerce-Price-currencySymbol { font-variation-settings: 'wdth' 100, 'wght' 500; }
.buy__price sup {
    font-size: 0.17em;
    vertical-align: super;
    letter-spacing: 0.12em;
}
/* The old price, struck through.
   It used to be 0.62 opacity, which on an accent band renders as a muddy grey
   smear against black type — the one grey thing on a page with no greys, and
   it read as broken rather than as superseded. It is now the band's own ink at
   full strength, small, with a rule through it: the same colour as everything
   else on the band, only quieter by SIZE. Colour carries state on this site;
   opacity does not. */
.buy__price del {
    font-size: 0.22em;
    letter-spacing: 0;
    opacity: 1;
    color: inherit;
    text-decoration-thickness: 0.08em;
    margin-right: 0.4em;
    vertical-align: 0.9em;
}
.buy__price ins { text-decoration: none; }
.buy__note {
    display: block;
    font-family: var(--f-mono);
    font-size: var(--t-nano);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--accent-on-dim);
    margin-top: var(--s-6);
}
.buy__note a { text-decoration: underline; text-underline-offset: 0.2em; }

/* Owned already — the buy block becomes a statement rather than a control. */
.buy--owned {
    background: transparent;
    color: var(--paper);
    border-top: var(--rule-w) solid var(--rule);
    border-bottom: var(--rule-w) solid var(--rule);
    cursor: default;
}
.buy--owned:hover { background: transparent; }
.buy--owned .buy__note { color: var(--paper-dim); }

/* ---- The sticky repeat -------------------------------------------------- */
/* ALWAYS UP.

   It used to slide in once the hero had left the screen and slide out again
   while the full-bleed .buy band was in view, on the argument that two of the
   same control on screen at once is one too many. It is now permanently on
   screen: the way to buy should not be something you have to scroll to find,
   and should not disappear at the exact moment you have scrolled to the part
   of the page that persuaded you.

   The consequence is that it covers the bottom 68px of the page forever, so
   the document reserves that height — see body:has(.buybar) below. Without it
   the last line of the colophon sat under the bar permanently, which was fine
   while the bar was transient and is not now.

   --buybar-h is stated here, beside the thing it measures, so the reservation
   and the bar cannot drift apart. */
:root { --buybar-h: calc(var(--tap) + var(--s-3) * 2); }

.buybar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 65;
    display: flex;
    background: var(--accent);
    color: var(--accent-on);
    /* No padding here: the button carries it, so the padded area is part of
       the click target rather than a dead margin around it. */
    padding: 0;
}
/* Kept because chrome.css and any older markup may still set it; it is now
   the state the bar is always in. */
.buybar.is-up { transform: none; }
.buybar:hover { background: var(--accent-hot); }

/* The page reserves the bar's height so nothing is permanently underneath it. */
body:has(.buybar) { padding-bottom: var(--buybar-h); }

.buybar__name {
    font-family: var(--f-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.16em;
    text-transform: uppercase;
}
button.buybar__go {
    /* Same override as button.buy__row: base.css would otherwise fill this
       with the accent on top of the accent band it already sits in.
       The padding is stated HERE rather than on `.buybar__go` because this
       selector is (0,1,1) and that one is (0,1,0) — with !important on both,
       the type selector wins, and a `padding: 0` left here collapsed the bar
       to a bare 44px with the click target ending at the text. */
    background: none !important;
    color: inherit !important;
    border: 0 !important;
    padding: var(--s-3) var(--gutter) !important;
    /* Size stated here too, and for the same specificity reason: base.css sets
       `button[type="submit"] { font-size: var(--t-label) }` at (0,1,1), which
       beat `.buybar__go` at (0,1,0) and collapsed the word "buy" to 12px. */
    font-size: clamp(1.3rem, 3vw, 2.4rem) !important;
    /* And the same for the distribution. base.css centres every submit button;
       this one is a full-bleed bar whose two ends are the name and the verb, so
       it has to push them apart. Three properties have now had to be restated
       on this selector for the same (0,1,1) vs (0,1,0) reason — anything set on
       `.buybar__go` alone that base.css also states will silently lose. */
    justify-content: space-between !important;
    width: 100% !important;
}

/* THE BAR AS A LINK, NOT A FORM. The cart's forward control is the same
   object as the product page's, but it navigates rather than submits, so it is
   an <a> wrapping a <span class="buybar__go">. The padding above is stated on
   `button.buybar__go` for a specificity reason that does not apply to a span,
   so it is stated here too — without it the span had no padding at all and the
   bar collapsed to the height of its text. */
.buybar--link { text-decoration: none; }
.buybar--link .buybar__go { padding: var(--s-3) var(--gutter); }

/* The button IS the bar: full width, and it owns the padding so every pixel of
   the accent band submits the form. */
.buybar__go {
    flex: 1 1 auto;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-6);
    min-height: var(--tap);
    text-align: left;
    cursor: pointer;
    font-family: var(--f-grotesk);
    font-variation-settings: 'wdth' 80, 'wght' 900;
    font-size: clamp(1.3rem, 3vw, 2.4rem);
    line-height: 1;
    letter-spacing: -0.02em;
    text-transform: uppercase;
}

/* The price inside the bar's name line inherits the mono register rather than
   the display one — it is a reminder, not the headline. The real price is the
   nine-rem one in the band. */
.buybar__name .woocommerce-Price-amount { font: inherit; }
.buybar__name del { opacity: 0.62; }
.buybar__name ins { text-decoration: none; }

@media (prefers-reduced-motion: reduce) {
    .buybar { transition: none; }
}

/* ==========================================================================
   5. SPEC LISTS
   --------------------------------------------------------------------------
   No bullets, no squares, no rules, no numbers. Bare lines. Every decoration
   that was here was a permanent mark nobody could edit or remove, and a page
   carrying a dozen of those reads as clutter rather than as a catalogue.
   ========================================================================== */

.spec li {
    font-family: var(--f-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--paper-dim);
    padding: 0.42em 0;
}

.kv {
    display: grid;
    grid-template-columns: minmax(6rem, 11rem) 1fr;
    gap: 0.3em var(--s-4);
    font-family: var(--f-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.06em;
    text-transform: uppercase;
}
.kv dt { color: var(--paper-faint); }
.kv dd { margin: 0; color: var(--paper-dim); }

@media (max-width: 560px) {
    .kv { grid-template-columns: 1fr; gap: 0 0; }
    .kv dt { margin-top: 0.9em; }
}

/* ==========================================================================
   5b. READING COPY — one register for every long-form body on the site
   --------------------------------------------------------------------------
   THE DOCUMENT ROUTES WERE STILL WEARING THE v5.0 LOOK, and it was the last
   place on the site that was.

   Everything the berlin system reads at length is set in Instrument Serif, in
   --paper, large: the product's description at 34px, its lede at 42px, a
   feature's copy at 30px, the support intro at 30px. Then /terms/, /privacy/,
   /accessibility/ and the support FAQ answers were Archivo at 17px in
   --paper-dim — a grey sans body at the size every website on earth uses. Not
   a variation on the system; the absence of it. A visitor moving from a
   product page to the terms went from a page with a voice to a page without
   one.

   legal.css argued for that in a comment: "the serif is the editorial signal
   — reserved for copy meant to persuade — and a refund policy is not that."
   The distinction is real but it was drawn in the wrong place. The serif is
   this site's READING face, not its selling face; the mono is what it uses
   when it is being technical. A contract is read, at length, by someone who
   needs to understand it — which is the case the serif is best at.

   Smaller than the product page's, because sixteen sections of clauses is not
   three sentences under a screenshot, and in --paper rather than --paper-dim:
   reading copy on this site is white, and a legal document is the last thing
   that should be set in the dimmer of two greys.
   ========================================================================== */

.sinemill-legal-body,
.sinemill-faq__answer,
.sinemill-doc-page #left-area .entry-content {
    font-family: var(--f-serif);
    font-size: clamp(1.2rem, 1rem + 0.55vw, 1.6rem);
    line-height: 1.45;
    letter-spacing: 0;
    color: var(--paper);
}

/* The measure is stated in `ch`, which resolves against the element's OWN
   font — so it re-measures itself now that the font is the serif, and stays a
   readable line however wide the window gets. */
.sinemill-legal-body p,
.sinemill-legal-body ul,
.sinemill-legal-body ol,
.sinemill-faq__answer p,
.sinemill-doc-page #left-area .entry-content p { max-width: 62ch; }

/* Mono for the technical asides inside a document — an email address, a VAT
   number, a file name — so the register still switches where it should. */
.sinemill-legal-body code,
.sinemill-faq__answer code {
    font-family: var(--f-mono);
    font-size: 0.82em;
    letter-spacing: 0.02em;
}

/* ==========================================================================
   6. COLOPHON — the site footer
   --------------------------------------------------------------------------
   Carries the legal surface (terms, privacy, accessibility, cookie withdrawal,
   trader identity) on every route where money changes hands, not only on the
   front page.

   IT USED TO HUDDLE. Three tracks — `auto auto 1fr` — put every word of it
   inside the left 540px of a 1440px page with the last track empty on purpose,
   in a site where nothing else is anything but full bleed. It read as a
   fragment of a footer that had lost its right-hand half, and it was the last
   block on EVERY route, so it was the last impression every route left.

   It is on the page's twelve columns now, like everything else, and it ends
   with the site saying its own name at display size — the masthead again at
   the other end of the page. The legal surface is unchanged; only where it
   sits and how loudly the name is set have moved.
   ========================================================================== */

/* z-index 2, NOT 3, AND THAT IS THE WHOLE FIX FOR THE BAR UNDER THE FOOTER.

   The layer model this theme runs on:

       1   .gl--back      the canvas planes draw behind text
       2   .colophon      the footer
       3   page content   (.ck, .doc, .pg's sections … between the canvases)
       5   .gl--front     the canvas that draws OVER text
      60   .nav
      65   .buybar        the fixed bars

   The cart's and the checkout's forward bars are `position: fixed` at 65, and
   they were painting UNDER the footer anyway. Not a z-index that was too low —
   one that was never consulted. Both bars are rendered from inside the page
   content by templates that have to be there (the cart's is inside
   `.cart_totals`, which WooCommerce replaces wholesale over AJAX; the
   checkout's is inside the form so its button can submit it), and page content
   carries `position: relative; z-index: 3` because it MUST sit above the back
   canvas, which is `position: fixed; inset: 0`. That makes it a stacking
   context, so the bar's 65 is resolved INSIDE it and the whole block competes
   with the footer at 3 — where the footer, being later in the document, wins.

   The bar cannot leave the content without breaking either the AJAX fragment
   or the form submit. The footer can move down a layer at no cost: it never
   overlaps the content geometrically, and 2 still clears the back canvas.

   The trap is the one chrome.css warns about at the top of the file. Read that
   note before changing any z-index here. */
.colophon {
    position: relative;
    z-index: 2;
    margin-top: var(--s-48);
    padding: var(--s-16) var(--gutter) var(--s-16);
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--gutter);
    row-gap: var(--s-12);
    align-items: end;
    font-family: var(--f-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--paper-dim);
}

.colophon h2 {
    font: inherit;
    color: var(--paper-faint);
    margin-bottom: 0.9em;
}
.colophon a { display: inline-block; padding: 0.18em 0; }
.colophon a:hover,
.colophon a:focus-visible { color: var(--accent-on); background: var(--accent); }
.colophon li { padding: 0.18em 0; }

/* ---- The name ------------------------------------------------------------ */

/* The site signs the page. It was 11px mono, the same size as the word "TERMS"
   beside it — the name of the company set at the size of a link to a policy. */
.colophon__brand { grid-column: 1 / 7; }
.colophon__brand h2 {
    font-family: var(--f-grotesk);
    font-variation-settings: 'wdth' 72, 'wght' 900;
    font-size: clamp(2.5rem, 8vw, 7rem);
    line-height: 0.82;
    letter-spacing: -0.03em;
    text-transform: lowercase;
    color: var(--paper);
    margin: 0;
}
.colophon__copy {
    margin: var(--s-4) 0 0;
    letter-spacing: 0.06em;
    color: var(--paper-faint);
}

/* ---- The legal surface --------------------------------------------------- */

.colophon__legalnav { grid-column: 9 / -1; }
.colophon__legalnav ul { margin: 0; padding: 0; list-style: none; }

/* Trader identity (e-Commerce Directive Art. 5) — permanently accessible on
   every route. Set below the columns because it is a legal disclosure rather
   than navigation, and it must not compete with the links for attention. */
.colophon__legal {
    grid-column: 1 / -1;
    padding-top: var(--s-6);
    /* The rule spans the page; the TEXT stops at a measure. `max-width` on
       this element clipped the border with it, so the page's closing line ran
       540px and then stopped in the middle of nowhere. */
    border-top: var(--rule-w) solid var(--rule-faint);
    font-size: var(--t-nano);
    letter-spacing: 0.05em;
    line-height: 1.8;
    text-transform: none;
    color: var(--paper-faint);
}
.colophon__legal > * { max-width: 80ch; margin: 0; }
.colophon__legal a { color: inherit; text-decoration: underline; }

@media (max-width: 860px) {
    .colophon { row-gap: var(--s-8); }
    .colophon__brand    { grid-column: 1 / -1; }
    .colophon__legalnav { grid-column: 1 / -1; }
}

/* Rendered unconditionally and shown by CSS ONLY under the reduced-motion
   media query — the browser's own answer to "is reduced motion on right
   now", so it cannot disagree with what the rest of the stylesheet is
   reacting to, and it stays correct if the setting changes without a reload. */
.sinemill-reduced-motion-notice { display: none; }
@media (prefers-reduced-motion: reduce) {
    .sinemill-reduced-motion-notice {
        display: block;
        grid-column: 1 / -1;
        font-size: var(--t-nano);
        letter-spacing: 0.05em;
        text-transform: none;
        color: var(--paper-faint);
    }
}

/* ==========================================================================
   7. THE EXPLORE BAR
   --------------------------------------------------------------------------
   Homepage counterpart to the product page's sticky buy bar. It names the
   plugin you are currently scrolled through and offers the way in.

   The attention-getter is the stenographer, not motion: when the plugin under
   the viewport changes the name is BACKSPACED and RETYPED in place. On a page
   where everything else drifts and shears, a word rewriting itself is the one
   gesture the eye has been trained to look at — and it costs no movement, so
   the bar never jitters while you are trying to read the page behind it.
   ========================================================================== */

.explore {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 65;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-6);
    background: var(--accent);
    color: var(--accent-on);
    padding: var(--s-3) var(--gutter) calc(var(--s-3) + 2px);
    transform: translateY(101%);
    transition: transform var(--d-bar) var(--e-out),
                background var(--d-snap) var(--e-hard);
}
.explore.is-up { transform: none; }
.explore:hover { background: var(--accent-hot); }

.explore__label {
    /* EXPLORE with a nib on it. Instrument Serif's ITALIC is the one face in
       the set with real stroke modulation — thick on the downstroke, hairline
       on the up — which is what makes a letterform read as written rather than
       set. The blackletter would be more literally quill-drawn and is not
       available: it is spent on the single interruption band, and a second
       appearance would stop that one being an interruption. */
    position: relative;
    font-family: var(--f-serif);
    font-style: italic;
    font-size: clamp(1.7rem, 4.6vw, 3.8rem);
    line-height: 0.95;
    letter-spacing: 0.01em;
    text-transform: uppercase;
    color: var(--accent-on);
    flex: 0 0 auto;
    padding-bottom: 0.18em;
}

/* The flourish. Writes itself in, holds while you read the name, unwrites.
   Seven seconds is deliberately slow: the bar sits at the bottom edge of every
   homepage screen for the whole visit, and anything quicker becomes a tic in
   peripheral vision rather than an invitation. */
.explore__quill {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 0.32em;
    overflow: visible;
}
.explore__quill path {
    fill: none;
    stroke: var(--accent-on);
    stroke-width: 4;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
    stroke-dasharray: 340;
    stroke-dashoffset: 340;
    animation: sm-quill 7s var(--e-out, ease) infinite;
}

@keyframes sm-quill {
    0%   { stroke-dashoffset: 340; }
    16%  { stroke-dashoffset: 0; }
    76%  { stroke-dashoffset: 0; }
    92%, 100% { stroke-dashoffset: -340; }
}

.explore__name {
    flex: 1 1 auto;
    font-family: var(--f-grotesk);
    font-variation-settings: 'wdth' 76, 'wght' 900;
    /* CAPITALS, and as large as the label. The product's name is the thing
       being offered; setting it smaller and lower-case than the word EXPLORE
       made the invitation louder than what it was inviting you to. */
    font-size: clamp(1.5rem, 4.2vw, 3.5rem);
    line-height: 0.95;
    letter-spacing: -0.02em;
    text-transform: uppercase;
    /* The name is rewritten character by character, so the box must not
       resize as it empties or the badges to its right will jump. */
    min-height: 1em;
}

/* AND IT NEEDS A BASELINE AS WELL AS A HEIGHT.

   min-height above holds the BOX open while the stenographer empties this
   element, and that was only half the problem. `.explore` is
   `align-items: baseline`, and an element with no text has no text baseline —
   the browser synthesises one at its bottom margin edge instead. So for the
   frames where textContent is "" the name's baseline dropped by most of a line,
   the flex line grew to keep the label aligned to it, and the whole bar jumped
   7px taller and back.

   Measured across a real scroll: 424 frames, 409 at 94.69px and 15 at 101.69px,
   and every one of the 15 had an empty name. One frame at a time, which is
   exactly how it was reported.

   A zero-width space in a pseudo-element guarantees a real line box, and so a
   real first-line baseline, whatever textContent happens to be at that instant.
   ::before rather than ::after because flex baseline alignment uses the FIRST
   line box, and a long name that wrapped would put ::after on the second.
   Invisible, zero-width, and not part of textContent, so steno.js neither sees
   it nor can erase it. */
.explore__name::before {
    content: "\200B";
}

/* --------------------------------------------------------------------------
   The badges took the arrow's place, and they are INVERTED.

   An arrow on a bar that spans the whole window and is entirely clickable
   states the obvious in the one position that could have carried something the
   reader does not know. These are that: what kind of thing this is, whether
   there is a sale on RIGHT NOW (PHP omits it otherwise, so a scheduled sale
   never appears early), and whatever the product itself is flagged with.

   Black plate, accent text — the exact reverse of the bar they sit on. An
   outline read as a faint stamp; a hole punched in the colour reads as an
   object, and it is the only place on the bar where the accent appears as
   INK rather than as ground.
   -------------------------------------------------------------------------- */

.explore__badges {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: var(--s-2);
    min-height: var(--tap);
}

.explore__badge {
    font-family: var(--f-grotesk);
    font-variation-settings: 'wdth' 88, 'wght' 800;
    font-size: clamp(0.7rem, 1.1vw, 0.95rem);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    background: var(--ink);
    color: var(--accent);
    border: 0;
    padding: 0.45em 0.8em 0.4em;
    white-space: nowrap;
}

/* Hover inverts the whole bar into the badges' colours, so the thing you are
   about to click confirms itself in the vocabulary it was already using. */
.explore:hover,
.explore:focus-visible {
    background: var(--ink);
    color: var(--accent);
}
.explore:hover .explore__label,
.explore:focus-visible .explore__label,
.explore:hover .explore__name,
.explore:focus-visible .explore__name { color: var(--accent); }
.explore:hover .explore__quill path,
.explore:focus-visible .explore__quill path { stroke: var(--accent); }
.explore:hover .explore__badge,
.explore:focus-visible .explore__badge { background: var(--accent); color: var(--ink); }

@media (max-width: 560px) {
    /* The name and the way in survive; the badges are the part you can read on
       the product page you are one tap from. The label does NOT go — it is the
       offer, and it is the whole point of the bar. */
    .explore__badges { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .explore { transition: none; }
    /* The flourish stops, drawn. Not hidden — it is part of the wordmark, and
       removing it would change the composition rather than just stilling it. */
    .explore__quill path { animation: none; stroke-dashoffset: 0; }
}

/* ==========================================================================
   8. INFO
   --------------------------------------------------------------------------
   The one block on the site that is about the person rather than the tools.
   Copy comes from the Customizer (Appearance › Homepage Info Section), whose
   defaults carry the real text — see sinemill_info_defaults().
   ========================================================================== */

.info__head {
    font-family: var(--f-grotesk);
    font-variation-settings: 'wdth' 70, 'wght' 900;
    font-size: var(--t-h1);
    line-height: 0.82;
    letter-spacing: -0.03em;
    text-transform: lowercase;
    position: relative;
    z-index: 3;
}

.info__mark {
    display: block;
    width: 100%;
    color: var(--accent);
    position: relative;
    z-index: 1;
}
.info__mark svg { width: 100%; height: auto; fill: currentColor; }
.info__mark img { width: 100%; height: auto; }

.info__bio {
    position: relative;
    z-index: 3;
    font-family: var(--f-serif);
    font-size: clamp(1.35rem, 0.9rem + 1.6vw, 2.5rem);
    line-height: 1.24;
}
.info__bio p + p { margin-top: 0.8em; }
.info__by {
    display: block;
    margin-top: 1.2em;
    font-family: var(--f-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--paper-dim);
}

.info__links {
    position: relative;
    z-index: 3;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.3em;
    font-family: var(--f-grotesk);
    font-variation-settings: 'wdth' 92, 'wght' 700;
    font-size: clamp(1.1rem, 2vw, 1.9rem);
    letter-spacing: -0.01em;
    text-transform: lowercase;
}
.info__links a { display: inline-block; }
.info__links a:hover,
.info__links a:focus-visible { background: var(--accent); color: var(--accent-on); }

/* ==========================================================================
   9. GRAIN
   --------------------------------------------------------------------------
   Rendered by grain.js. `screen`, never `overlay`: overlay against a true
   black ground computes to exactly zero, so the grain would be
   mathematically invisible across the large majority of the page — precisely
   where a near-black print most needs texture.

   z-index 950 puts it over everything including the loader; the whole site
   is one photographed surface, not a page with a texture on part of it. It
   sits under the toast alone, which carries information rather than
   atmosphere and has to stay crisp.
   ========================================================================== */

.grain {
    position: fixed;
    inset: 0;
    z-index: 950;
    width: 100%;
    height: 100%;
    pointer-events: none;
    opacity: var(--grain);
    mix-blend-mode: screen;
}

/* ==========================================================================
   10. GENERIC PAGE
   --------------------------------------------------------------------------
   page.php serves every WordPress Page that has no custom template — a page
   the owner creates in the editor and never thinks about again. Only the
   always-loaded stylesheets are guaranteed on that route, so its furniture
   lives here rather than in a per-route sheet that would not be enqueued.

   Without this the route falls back to the browser's defaults: Times New
   Roman on a black ground, full window width, no clearance under the corner
   nav. That is not a hypothetical — it is what /sample-page/ rendered.

   EVERY SELECTOR IS SCOPED TO .sinemill-doc-page, and that scope is
   load-bearing rather than tidy. page.php ALSO serves cart, checkout and
   account, whose template output lands inside this same .entry-content —
   unscoped, `.entry-content { color: var(--paper-dim) }` greyed out the
   cart's own heading and `.entry-content a { text-decoration: underline }`
   put underlines through the product links and the continue-shopping button.
   Those routes have stylesheets that own them; this is only the floor under
   the pages that have none.

   The class is added to <body> by sinemill_doc_page_body_class() rather than
   being derived here from :not(.woocommerce), because "which routes are
   WooCommerce" is a question PHP can answer exactly and a selector can only
   approximate.
   ========================================================================== */

.sinemill-doc-page #left-area {
    position: relative;
    z-index: 3;
    padding: var(--nav-clear) var(--gutter) var(--s-32);
    max-width: calc(var(--measure) + var(--gutter) * 2);
    /* Centred, like every other measure on the site. A max-width with no auto
       margin is a left-aligned column, and on a 1600px screen that stranded
       these pages in the left third with two thirds of empty page beside
       them. */
    margin-inline: auto;
}

.sinemill-doc-page .entry-title {
    font-family: var(--f-grotesk);
    font-variation-settings: 'wdth' 70, 'wght' 900;
    font-size: var(--t-h1);
    line-height: 0.84;
    letter-spacing: -0.03em;
    text-transform: lowercase;
    margin-bottom: var(--s-8);
}

.sinemill-doc-page .entry-content {
    font-size: var(--t-body);
    line-height: 1.6;
    color: var(--paper-dim);
}

.sinemill-doc-page .entry-content > * + * { margin-top: 0.9em; }
.sinemill-doc-page .entry-content p { max-width: 68ch; }

.sinemill-doc-page .entry-content h2,
.sinemill-doc-page .entry-content h3,
.sinemill-doc-page .entry-content h4 {
    font-family: var(--f-grotesk);
    font-variation-settings: 'wdth' 80, 'wght' 900;
    line-height: 1.05;
    letter-spacing: -0.02em;
    text-transform: lowercase;
    color: var(--paper);
    margin-top: var(--s-12);
}
.sinemill-doc-page .entry-content h2 { font-size: var(--t-h3); }
.sinemill-doc-page .entry-content h3 { font-size: 1.35rem; }
.sinemill-doc-page .entry-content h4 { font-size: 1.1rem; }

.sinemill-doc-page .entry-content strong { color: var(--paper); font-weight: 700; }
.sinemill-doc-page .entry-content a { color: var(--paper); text-decoration: underline; text-underline-offset: 0.2em; }

.sinemill-doc-page .entry-content li { padding: 0.2em 0 0.2em 1.1em; position: relative; }
.sinemill-doc-page .entry-content ul li::before { content: "—"; position: absolute; left: 0; color: var(--paper-faint); }
.sinemill-doc-page .entry-content ol { counter-reset: ec; }
.sinemill-doc-page .entry-content ol li { counter-increment: ec; }
.sinemill-doc-page .entry-content ol li::before {
    content: counter(ec) ".";
    position: absolute;
    left: 0;
    font-family: var(--f-mono);
    font-size: var(--t-micro);
    color: var(--paper-faint);
}

.sinemill-doc-page .entry-content blockquote {
    padding-left: var(--s-4);
    border-left: 3px solid var(--rule);
    color: var(--paper-faint);
}
.sinemill-doc-page .entry-content code {
    font-family: var(--f-mono);
    font-size: 0.9em;
    background: var(--lift-2);
    padding: 0.1em 0.35em;
}
.sinemill-doc-page .entry-content pre {
    background: var(--lift-1);
    border: var(--rule-w) solid var(--rule-faint);
    padding: var(--s-4);
    overflow-x: auto;
}
.sinemill-doc-page .entry-content img { margin: var(--s-6) 0; }

/* ==========================================================================
   11. CONSENT BANNER
   --------------------------------------------------------------------------
   Markup and behaviour come from sinemill-core (Sinemill_Compliance), which
   ships its own assets/css/consent.css. That file is outside this repo and
   still speaks the v5.0 vocabulary — rounded buttons, the old sans, a tinted
   panel — so on every single page the first thing a new visitor saw was the
   one element that had not been ported.

   Restyled from here rather than by editing the plugin: the plugin is the
   compliance surface and the theme is the design surface, and a consent
   banner that stops working because someone was adjusting its border radius
   is a regulatory problem, not a visual one. Its stylesheet loads first, so
   these override on equal specificity by order.

   Deliberately NOT dismissible by clicking away, NOT pre-selected, and both
   choices given equal prominence in SIZE — consent has to be as easy to
   refuse as to give. "Accept" carries the accent because it is the primary
   action, not because it is the preferred one; "essential only" is a full
   control beside it, not a link hidden underneath.
   ========================================================================== */

.sinemill-consent {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* Under the toast (960) and the grain (950), above the sticky bars (65) —
       it is the most important thing on screen until it is answered. */
    z-index: 940;
    background: var(--ink);
    border-top: var(--rule-w) solid var(--rule);
    border-radius: 0;
    box-shadow: none;
    padding: var(--s-4) var(--gutter);
}

.sinemill-consent__inner {
    display: flex;
    /* STATED, and it has to be. sinemill-core's own consent.css still carries
       `@media (max-width: 600px) { .sinemill-consent__inner { flex-direction:
       column } }` from the layout this replaced. That plugin sheet loads
       FIRST, so every property the theme restates wins — but flex-direction
       was not one of them, and an omission is not an override. Below 600px
       the container was therefore a COLUMN while the rules describing its
       children assumed a row.

       What that cost: `.sinemill-consent__text` is `flex: 1 1 24rem`, and a
       flex-basis is measured along the MAIN axis. In a row 24rem is a width,
       which is what it was written to be. In a column it is a HEIGHT — so on
       a phone the paragraph claimed a 384px floor for about 90px of type and
       the banner opened 485px tall on an 844px screen, five lines of text and
       two buttons with 350px of black between them.

       `row` plus the `flex-wrap: wrap` below gets the intended result without
       the trap: at 390px the text's 24rem basis exceeds the line, so it wraps
       to its own full-width row at its natural height and the buttons follow
       underneath. The stack is the same; the phantom height is gone. */
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-6);
    flex-wrap: wrap;
    max-width: 1400px;
    margin: 0 auto;
}

.sinemill-consent__text {
    flex: 1 1 24rem;
    min-width: 0;
    font-family: var(--f-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.04em;
    line-height: 1.7;
    color: var(--paper-dim);
}
.sinemill-consent__text a {
    color: var(--paper);
    text-decoration: underline;
    text-underline-offset: 0.2em;
}

.sinemill-consent__actions {
    display: flex;
    gap: var(--s-2);
    flex-wrap: wrap;
    flex: 0 0 auto;
}

.sinemill-consent__btn {
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 var(--s-6);
    border-radius: 0;
    border: var(--rule-w) solid transparent;
    font-family: var(--f-mono);
    font-size: var(--t-micro);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    /* Colour only. A consent control that moves under the cursor is a consent
       control that can be clicked by accident. */
    transition: background var(--d-snap) var(--e-hard),
                color var(--d-snap) var(--e-hard);
}

.sinemill-consent__btn--accept {
    background: var(--accent);
    color: var(--accent-on);
    border-color: var(--accent);
}
.sinemill-consent__btn--accept:hover { background: var(--accent-hot); color: var(--accent-on); }

.sinemill-consent__btn--ghost {
    background: transparent;
    color: var(--paper);
    border-color: var(--rule);
}
.sinemill-consent__btn--ghost:hover { background: var(--paper); color: var(--ink); }

@media (max-width: 640px) {
    .sinemill-consent__actions { width: 100%; }
    .sinemill-consent__btn { flex: 1 1 auto; }
}
