/* Generated by tools/build-stylesheet.sh — do not change by hand. */
@layer tokens, base, elements, widgets, slices, layouts, templates;

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The token set — measures from the golden ratio, colours in OKLCH.

   Everything else derives from here. No package sets a number of its own
   where a token already carries it: a size that stands in only one place is
   a size that does not move along at the next pass. */

@layer tokens {
    :root {
        /* **Dark, and always.** `dark light` meant: the browser may choose by
           the system setting — and on a light Windows it chose light,
           whereupon every dark face turned almost white and the text on it
           unreadable (reported on 3 September 2026). A light theme is
           thought ahead, not decided: the light values in `light-dark()`
           stay, so that they can be found some day without searching, but
           served is the dark version. One word here turns that around
           (vlh T0026S12). */
        color-scheme: dark;

        /* Measure ladder — the step is √φ = 1.272, two steps are φ. Type takes
           every point, spacing every second. */
        /* The base grows with the width: 14 px on the phone, 15 from tablet,
           17 from screen. Measured on the running text of the design in all
           three versions — it scales the type instead of setting it the same
           everywhere, and without that the page looks a fifth too large on
           the phone.

           The ladder itself stays: 17 by √φ is 13.4; times √φ is 21.6;
           times φ is 27.5 — the design hits 13, 22 and 28. The steps below
           stand at the breakpoints. */
        --scale-base:  0.875rem;

        /* Two sizes do not follow the ladder and therefore stand per width:

           The stage — 23, 30, 44 px. It is the game name and should fill
           the bar, which looks different at 430 px than at 1440.

           The page heading — 19, 22, 28 px. Its ratio to the base grows
           with the width (1.36 · 1.47 · 1.65 instead of a constant φ), and
           that is no carelessness of the design: a heading that carried the
           same multiple on the phone as on the desk would take half the
           height of the viewport there. Where there is little room, it moves
           closer to the text. */
        --text-stage:  1.4375rem;
        --text-head:   1.1875rem;

        /* Labels in capitals — card titles, metric labels. 10, 11, 12 px: they
           grow more slowly than the base, because capitals build wider at
           the same size and would otherwise wrap in a tight card. */
        --text-label:  0.625rem;

        --text-micro:  calc(var(--scale-base) / 1.618);   /* 10.5 → labels */
        --text-small:  calc(var(--scale-base) / 1.272);   /* 13.4 → tags, card titles */
        --text:        var(--scale-base);                 /* 17   → running text */
        --text-large:  calc(var(--scale-base) * 1.272);   /* 21.6 → metrics */
        --text-title:  calc(var(--scale-base) * 2.058);   /* 35   → the stage */

        --space-hair:  calc(var(--scale-base) * 0.146);
        --space-tight: calc(var(--scale-base) * 0.236);
        --space-snug:  calc(var(--scale-base) * 0.382);
        --space-close: calc(var(--scale-base) * 0.618);
        --space:       var(--scale-base);
        --space-open:  calc(var(--scale-base) * 1.618);

        /* The height of every control — field, button, and whatever comes.
           
           It stood in the seed_form widget before, which stretched field and
           button in one grid row. On extracting into elements it got lost:
           on the landing page the button stretched over label **and** field,
           on the item page it shrank to its content. Two pages, two heights,
           one thing.
           
           The landing page is the model, and this number is measured from
           there: 0.9375rem of padding top and bottom plus 0.8125rem of type. */
        --control-height: 2.6875rem;

        /* The height of the sticky head (vlh T0097). Measured on the running
           page: 68 points — two times 1rem of padding, 30 points of mark,
           5 points of progress bar, 1 point of border. It stands here
           because something floating under the head has to know it and
           cannot ask: `position: fixed` refers to the window, and CSS cannot
           write a measured height into a property.

           **Whoever changes the head changes this number.** That is the
           price; the alternative would be script that reads a height and
           writes it back — more mobility for more parts. */
        --head-height: 4.25rem;
        --space-wide:  calc(var(--scale-base) * 2.618);

        /* The sheet — the measures of every popover that opens (vlh T0084).

           They stood in the item_sheet widget, measured by the owner on
           1 September 2026. When the map got its own sheet, I copied the
           same six numbers a second time, and with that there were two
           truths about one thing: whoever changes the width changes it in
           one place and sees the next day that the other stayed. Exactly
           the case vlh K0054 describes — only that the shell is no markup one
           could lift into an element, but measure. And measure lives here —
           like `--control-height` above.

           Whoever builds a sheet writes these tokens and no numbers. */
        --sheet-inline-size:     min(400px, calc(100vw - 2rem));
        --sheet-max-block-size:  min(900px, calc(100vh - 2rem));
        /* **The edge of everything that lies on the page** — sheets and
           bands. The name says »sheet«, because the first one was there;
           meant is every element that floats above the content and
           therefore has to set itself off from it (vlh T0097).

           Whoever builds one writes this token and no number. Until
           12 September 2026 it did not stand in two places: the map's
           favourites sheet carried its 3px written out, and the notice band
           a single one — and a band with a hairline beside a sheet with
           three points looks like two house orders. */
        --sheet-edge:            3px;
        --sheet-shadow:          0 24px 64px oklch(0% 0 0 / 0.55);
        --sheet-scrim:           oklch(0% 0 0 / 0.6);
        /* Belt and braces: in the top layer a popover needs no z-index, but on
           closing it drops out, and then it decides after all (Owner,
           1 September 2026). */
        --sheet-layer:           9999;

        /* Output width. A single cap: the slice's track bounds itself by
           min\(\), hence the width needs no four steps — the gutter grows,
           not the cap. */
        /* The design leaves the content about 40 px of edge at 1440 px, so
           uses almost the full width. 84rem hits that and at the same time
           caps what would otherwise run boundless on a very wide screen —
           the running text stays at 66ch anyway \(see prose\). */
        --measure:     90rem;
        --gutter:      2.5rem;

        /* Grid of the pixel font. Its glyphs are eight font pixels wide; for an
           edge to fall on a whole device pixel, size by eight times device
           resolution must be a whole number — at resolution 1 and 3 a
           multiple of 8, at 2 one of 4 \(vlh K0044\). The ladder stays the
           same; rounding happens at the place of use with round\(\). */
        --pixel-grid:  8px;

        /* Surface and type are two scales, not one. The design sets the
           surfaces close together — 12 to 21 percent, five steps on nine
           points — and the type far above. A shared scale would have to
           serve both and would hit neither.

           The values for dark are measured from the design, those for light
           mirrored on them. light-dark() takes the first value in the light
           scheme. */
        --hue-surface: 260;
        --c-surface:   0.010;

        --surface-0:  light-dark(oklch(99% var(--c-surface) var(--hue-surface)),
                                 oklch(12% var(--c-surface) var(--hue-surface)));
        --surface-1:  light-dark(oklch(97% var(--c-surface) var(--hue-surface)),
                                 oklch(15% var(--c-surface) var(--hue-surface)));
        --surface-2:  light-dark(oklch(94% var(--c-surface) var(--hue-surface)),
                                 oklch(17% var(--c-surface) var(--hue-surface)));
        --surface-3:  light-dark(oklch(91% var(--c-surface) var(--hue-surface)),
                                 oklch(18% var(--c-surface) var(--hue-surface)));
        --surface-4:  light-dark(oklch(86% var(--c-surface) var(--hue-surface)),
                                 oklch(21% var(--c-surface) var(--hue-surface)));

        /* Type: three steps, measured from the design. 65 carries the text, 58
           the trimmings, 55 the quietest — mirrored in the light scheme. */
        /* Three steps, all measured from the export. The lightest carries
           another hue — 90 instead of 260 —, because a heading on dark
           ground should look warm and not blue.

           There were four. The quietest, --ink-faint, lay at 48 percent in
           dark mode and gave only 3.02 to 1 on --surface-0 — under AA, at
           all five places where it stood. Measured, 58 percent are needed
           there, and that is exactly --ink-quiet: below this step there is
           no text colour on this ground any more that holds the limit. A
           step that is nowhere usable within the rules is no step. */
        --ink-bright: light-dark(oklch(20% 0.005 90),  oklch(95% 0.005 90));
        --ink:        light-dark(oklch(28% 0.008 var(--hue-surface)),
                                 oklch(70% 0.008 var(--hue-surface)));
        /* The quiet step stood at 58 percent and held AA only on --surface-0
           — on the raised faces it fell to 4.43 and 4.36, measured at nine
           places. The ground is lighter there, so the type has to be too.
           61 percent carries on all three with reserve; 60 would have
           sufficed, but at 4.74 would again lie close to the edge. */
        --ink-quiet:  light-dark(oklch(42% 0.008 var(--hue-surface)),
                                 oklch(61% 0.008 var(--hue-surface)));

        --ground:     var(--surface-0);
        --ground-up:  var(--surface-2);

        /* **Three role colours, and there were always three** (Owner,
           12 September 2026: »es gibt folgende Grünwerte und das von Anbeginn
           … es gab niemals ein weiteres Grün oder ähnliches«).

           | Primary Green  | oklch(76% 0.17 130) | what leads, what triggers |
           | Secondary Gold | oklch(80% 0.13 85)  | what shows more           |
           | Tertiary Orange| oklch(68% 0.18  35) | the mark, sparingly       |

           Until then a fourth token stood here, `--action` with
           `oklch(72% 0.20 145)`, and beside it an `--on-action` nobody ever
           used. The sentence that justified both — »Verweis führt weg,
           Aktion löst aus« — described a division that never existed: the
           action button carried `--link` all along, and `--action` stood in
           twenty places almost only in focus rings. Two greens, one pixel
           apart in meaning and clearly so to the eye (vlh K0233, vlh T0096).

           **That holds for the roles of the surface, not for data.** The
           biome colours of the map and the rank ladder white-green-blue-
           magenta follow the game and stay where they are. */
        --brand:      oklch(68% 0.18  35);
        --link:       oklch(76% 0.17 130);
        /* Golden yellow, the third role beside the two greens: it says
           neither »leads away« nor »triggers« but »shows more«. The chevron
           on the map button carries it \(Owner, 1 September 2026\). */
        --gold:       oklch(80% 0.13 85);
        /* Red, the fourth role: »takes away«. The button in the target's sheet
           so leads out of the building plan back to the item page \(Owner,
           8 September 2026, vlh T0073S13\). Measured on --surface-4 with dark
           type: 6.1 to 1. */
        --danger:     oklch(66% 0.19 28);

        /* The rank ladder of quality: white, green, blue, magenta — the
           sequence World of Warcraft and Elder Scrolls Online taught every
           player, chosen by the owner \(vlh T0023S4, 02-valheim.md\). It says
           **how good** a piece is; the biome colours say **when** in the
           game. Two systems, two questions — and hence no biome tone here.

           Green and magenta are measured from the design: rgb\(0,255,28\)
           and rgb\(255,0,252\). The design's blue, rgb\(11,97,172\), holds
           only 2.8 to 1 on surface-4 — under AA even for large type. It
           therefore stands at 65 percent lightness in the same hue: 5.5 to
           1, and still the blue of the ladder. The deviation is measured and
           named \(AW22\). White is the light ink the name carries anyway. */
        --rank-1:     var(--ink-bright);
        --rank-2:     oklch(86.7% 0.291 143);
        --rank-3:     oklch(65% 0.14 252);
        --rank-4:     oklch(70% 0.321 329);

        /* The scanline grid. One dark line in three, at 14 percent — on
           average 4.7 percent of darkening.

           **Where a picture gets a raster, it comes from here.** Not copied
           out, not adjusted by eye: the values are tight, and lying beside
           them does not look like »somewhat different« but like another
           effect. The biome bar stood at one line in two at 28 percent —
           three times as dark, and because line and gap run into each other
           on a double-resolution screen, nothing of the grid was left to
           see. Only a dark picture \(vlh K0097\).

           `linear-gradient` with `background-size` and not
           `repeating-linear-gradient`: a tile the browser does not compute
           itself does not let one drop out every few lines.

           To be used as a background layer, not as an overlay in a
           pseudo-element — the layer lies in the same `background` as the
           picture and cannot deviate from it. */
        --interlace:      linear-gradient(180deg,
                              oklch(0% 0 0 / 0.14) 0 1px, transparent 1px 3px);
        --interlace-size: 100% 3px;
    }

    /* The ladder has five steps — base · phone 16.25rem · tablet 35rem ·
       screen 60rem · widescreen 78.75rem. The gutter grows at two of them;
       at the others deliberately nothing stands, because nothing changes
       there. Only deltas, only min-width. */
    /* On the phone the gutter is narrower, otherwise nothing is left of the
       content. From tablet on it stands at the design's 40 px and grows no
       further — what room there is beyond, the cap measure takes.

       Only deltas, only min-width: every step names what changes, and
       nothing else. */
    :root { --gutter: var(--space); }

    @media (min-width: 35rem) {
        :root {
            --gutter:     2.5rem;
            --scale-base: 0.9375rem;
            --text-stage: 1.875rem;
            --text-head:  1.375rem;
            --text-label: 0.6875rem;
        }
    }

    @media (min-width: 60rem) {
        :root {
            --scale-base: 1.0625rem;
            --text-stage: 2.75rem;
            --text-head:  1.75rem;
            --text-label: 0.75rem;
        }
    }

    @media (resolution >= 2x) { :root { --pixel-grid: 4px; } }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The foundation — box model, base type, and the tracks of every slice. */

@layer base {
    *, *::before, *::after { box-sizing: border-box; }

    :where(body, h1, h2, h3, h4, p, figure, ul, ol, dl, dd) { margin: 0; }
    :where(ul[role="list"], ol[role="list"]) { padding: 0; list-style: none; }

    html {
        font-size: 100%;
        -webkit-text-size-adjust: none;
    }

    body {
        font-family: "Oxanium", "Oxanium Fallback", system-ui, sans-serif;
        font-size: var(--text);
        line-height: 1.5;
        color: var(--ink);
        background: var(--ground);
    }

    :where(img, picture, svg) { display: block; max-inline-size: 100%; }
    :where(input, button, select, textarea) { font: inherit; color: inherit; }

    /* A slice is a row across the full width \(vlh K0031\). The bounding is
       carried by the named track, not a wrapper: children land in
       `content`, and what should run edge to edge says `grid-column: full`.

       Expressly no width statement — the slice is a child of the
       single-column grid and fills its track. `width: 100vw` would include
       the scrollbar and give about 15 px of horizontal overflow on the
       desk, which does not occur on the phone and is therefore noticed late. */
    [data-slice] {
        display: grid;

        /* The edge is computed and stands ready as a variable. That is the
           difference that counts: a band that wants to share inside only
           replaces the content track and inherits the same edge. As
           minmax\(gutter, 1fr\) the gutter would fight with every inner fr
           track over the room, and every band would come to another edge —
           measured 40, 63, 77 and 110 px on four bands of the same page
           \(vlh K0057\). */
        --edge: max(var(--gutter), (100% - var(--measure)) / 2);

        grid-template-columns:
            [full-start] var(--edge)
            [content-start] minmax(0, 1fr)
            [content-end] var(--edge)
            [full-end];
    }

    [data-slice] > * { grid-column: content; }
}

/* As long as a sheet stands in the top layer, the page beneath does not move.

   A popover does **not** lock scrolling by itself — unlike a <dialog> in
   the modal state. `:has()` does it without script, and the rule holds for
   every popover, not only for this one \(Owner, 1 September 2026\).

   `scrollbar-gutter: stable` stands beside it, so that the vanishing of the
   scrollbar does not let the page jump by its width. */
/* **The scrollbar's room is always reserved, not only once it is gone.**
   Exactly here `scrollbar-gutter: stable` stood at first — in the rule
   below, so valid only once the sheet was open. That is too late: the bar
   vanishes at the same moment the reservation takes hold, and the page
   jumps by its width. It has to be there beforehand. */
:root {
    scrollbar-gutter: stable;
}

/* As long as a sheet stands in the top layer, the page beneath does not move.
   A popover does **not** lock scrolling by itself — unlike a <dialog> in
   the modal state. Both elements, because depending on the browser the one
   or the other scrolls. */
:root:has([popover]:popover-open),
:root:has([popover]:popover-open) body {
    overflow: hidden;
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The theme for Valheim — the four things that change per game.

   Structure is general, colours are specific. A second game lays a file
   beside it and changes nothing else; that is the whole theme capability
   (vlh K0029). What stands here is the palette — icons, map and biome colours
   come from the game data and not from the stylesheet. */

@layer tokens {
    [data-theme="valheim"] {
        /* The stage bar — copper, like the first ore one smelts in the game.
           Eight bands with hard edges, symmetrical around the middle:
           measured from the design, not imitated. The hue moves from 38 at
           the edges to 55 in the middle, which gives the metal its shimmer —
           a gradient on a single tone would look like a colour area, not
           like light. */
        /* The scanlines lie above the gradient: one pixel of black at 28
           percent, two pixels of gap. That is the screen the type means —
           without them the bar is a colour area with stripes, with them it
           is a monitor. Measured from the design.

           As a second background layer instead of an absolutely positioned
           pseudo-element: saves a node and cannot slip.

           A plain gradient with a fixed tile size, not repeating: a
           continuous gradient stretches over the whole area and rounds its
           periods against the device pixel grid — every few lines one drops
           out. With `background-size: 100% 3px` the tile is exactly three
           pixels tall and repeats cleanly. */
        --stage-scan:
            linear-gradient(180deg,
                oklch(0% 0 0 / 0.28) 0 1px,
                transparent 1px 3px);

        /* The peak stood at 74 percent and was the lightest spot of the bar —
           exactly where the title stands. Measured that gave 2.19 to 1
           against the title colour, needed are 3.0 for large type. The row
           only crossed the limit from 64 percent on; 62 gives reserve, so
           that a later type size does not tear it again at once.

           The bar so becomes somewhat richer and less glaring. That is a
           deviation from the design, it stands here, and it is measured — as
           AW22 demands. The design meets no AA at this point. */
        --stage-bar:
            linear-gradient(180deg,
                oklch(30% 0.09 40) 0 14%,
                oklch(45% 0.14 42) 0 28%,
                oklch(58% 0.18 45) 0 42%,
                oklch(62% 0.17 55) 0 50%,
                oklch(58% 0.18 45) 0 58%,
                oklch(45% 0.14 42) 0 72%,
                oklch(28% 0.08 38) 0 86%,
                oklch(16% 0.05 35) 0 100%);
        --stage-ink:   oklch(97% 0.02 40);

        /* The ten biomes. The order is that of the game's progress, not of
           the alphabet — whoever reads them reads the way. */
        --biome-meadows:   oklch(74% 0.14 130);
        --biome-forest:    oklch(52% 0.10 150);
        --biome-swamp:     oklch(44% 0.07  95);
        --biome-mountain:  oklch(88% 0.03 230);
        --biome-plains:    oklch(80% 0.13  85);
        --biome-mistlands: oklch(46% 0.05 280);
        --biome-ashlands:  oklch(58% 0.19  35);
        --biome-deepnorth: oklch(84% 0.05 210);
        --biome-ocean:     oklch(56% 0.10 235);
        --biome-edge:      oklch(38% 0.02 260);
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The beta mark — a Workbench gadget in red. */

@layer elements.beta_badge {
    @scope ([data-type="beta-badge"]) {
        /* The three reds stand here and not in tokens.css. The sentence above
           once read »waechst daraus eine zweite Stelle, wandern sie hoch«,
           and with the status window \(vlh T0097\) the second place came — the
           move into `:root` was wrong all the same. **AW10 and vlh K0004 forbid
           derivations in `:root`**: a computed tone stands at every context
           that sets the base colour, so that it follows when that changes.
           The status window computes the same line itself. The same
           calculation in two places is the price, and it is smaller than a
           value that freezes.

           Chosen after the Amiga: the face is the palette's red darkened, the
           light edge lies above it, the shadow edge below. White on the
           ground measures 6.66 to 1 and so carries even the smallest step. */
        :scope {
            /* **Computed from the palette, not written beside it**
               (vlh T0096, AW10). Three red values of their own stood here, and
               the sentence above called them »das satte Rot der Palette« —
               they were not: `--danger` stands at 66 % lightness, here stood
               50, and the hue differed by a degree. No design intent, a
               copying error.

               The face is darker than `--danger`, and that stays: it carries
               white type, and for that it needs the distance. The two edges
               compute symmetrically from it — thirteen points lighter above,
               thirteen darker below. */
            --badge-face:   oklch(from var(--danger) 0.50 c h);
            --badge-light:  oklch(from var(--badge-face) calc(l + 0.13) c h);
            --badge-shadow: oklch(from var(--badge-face) calc(l - 0.13) c h);

            display: inline-block;
            padding: 0.375rem 0.5rem 0.3125rem;
            background: var(--badge-face);
            color: oklch(100% 0 0);

            /* The Workbench box: a black line outside, the two edges inside.
               Both without a radius — there is no rounded corner on a device
               with 320 by 256 points. */
            border: 1px solid oklch(0% 0 0);
            box-shadow:
                inset  2px  2px 0 0 var(--badge-light),
                inset -2px -2px 0 0 var(--badge-shadow);

            font-family: "Press Start 2P", monospace;
            font-weight: 400;
            font-size: 0.5rem;
            line-height: 1;
            letter-spacing: 0.08em;
            text-transform: uppercase;

            /* The baseline of the pixel font sits low; without the correction
               the mark hangs under the word mark instead of beside it. */
            translate: 0 -0.0625rem;
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: Short labels — without a frame, the colour carries the statement. */

@layer elements.chip {
    @scope ([data-type="chip"]) {
        :scope {
            font-family: ui-monospace, monospace;
            font-size: var(--text-micro);
            letter-spacing: 0.16em;
            text-transform: uppercase;
            color: var(--link);

            /* No frame and no face. A label that already stands out of the text
               through type, colour and capitals needs no second carrier —
               the frame costs room and carries nothing. */
        }

        /* Gold instead of green, as on the button and the switch (vlh K0032,
           vlh T0093). On the map it separates the switch group that asks for
           the place from the one that asks for the kind: the label carries
           the same tone as the switches below. Only the colour changes —
           type, tracking and capitals stay, because it is the same label. */
        :scope[data-tone="gold"] { color: var(--gold); }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The donate button — white, rounded, the cup in front. */

@layer elements.donate_button {
    @scope ([data-type="donate-button"]) {
        :scope {
            /* The measure comes from the context, the form the button computes
               from it: rounding, padding and cup grow with the height.
               Without a value it is as tall as every control; the map's
               footer strip gives it a lower one. */
            --height: var(--donate-height, var(--control-height));

            /* Symbol beside text: the one row for which flex is allowed. */
            display: inline-flex;
            align-items: center;
            gap: calc(var(--height) * 0.22);
            block-size: var(--height);
            padding-inline: calc(var(--height) * 0.3) calc(var(--height) * 0.42);

            /* White and Ko-fi's dark, 32 32 32 in RGB — measured 16 to 1. */
            background: oklch(100% 0 0);
            color: oklch(24.4% 0 0);
            border-radius: calc(var(--height) * 0.3);

            font-size: var(--donate-font, var(--text-small));
            font-weight: 700;
            line-height: 1;
            white-space: nowrap;
            text-decoration: none;
        }

        img {
            block-size: calc(var(--height) * 0.52);
            inline-size: auto;
        }

        /* On pointing the face steps back a touch and the cup wobbles, as on
           Ko-fi itself. The face jumps, the cup moves only through
           transform (vlh K0036). */
        :scope:where(:hover, :focus-visible) {
            background: oklch(93% 0 0);
        }

        :scope:where(:hover, :focus-visible) img {
            animation: cup-wobble 600ms ease-in-out;
        }

        /* The focus ring reads the one green like every other (vlh K0234). */
        :scope:focus-visible {
            outline: 2px solid var(--link);
            outline-offset: var(--space-tight);
        }

        @media (prefers-reduced-motion: reduce) {
            :scope:where(:hover, :focus-visible) img { animation: none; }
        }
    }

    /* `@keyframes` knows no scope and therefore stands beside it, with a
       name that says whom it belongs to. */
    @keyframes cup-wobble {
        0%, 100% { transform: rotate(0deg); }
        25%      { transform: rotate(-12deg); }
        50%      { transform: rotate(10deg); }
        75%      { transform: rotate(-6deg); }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: Item icons from the game. */

@layer elements.game_icon {
    @scope ([data-type="game-icon"]) {
        :scope {
            inline-size: var(--space-wide);
            block-size: auto;

            /* The sources are pixel graphics. Without this line the browser
               interpolates when scaling up and turns hard edges into mush —
               the same problem as with the pixel font, only without rounding
               as a way out \(vlh K0044\). */
            image-rendering: pixelated;
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The fonts — served ourselves, with a metric fallback.

   No element in the document head is rendered, hence no scope stands here:
   what this package contributes in CSS is the font declarations themselves. */

@layer tokens {
    @font-face {
        font-family: "Oxanium";
        src: url("/fonts/oxanium.woff2") format("woff2");
        font-weight: 200 800;
        font-display: swap;
    }

    @font-face {
        font-family: "Press Start 2P";
        src: url("/fonts/press-start-2p.woff2") format("woff2");
        font-weight: 400;
        font-display: swap;
    }

    /* The metric fallback: it takes the jump out of the font swap without
       hiding the text (vlh K0039). The values are measured on Oxanium — ascent
       790, descent 210, line gap 250 of 1000 units. */
    @font-face {
        font-family: "Oxanium Fallback";
        src: local("Helvetica Neue"), local("Arial");
        ascent-override: 79%;
        descent-override: 21%;
        line-gap-override: 25%;
        size-adjust: 99%;
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: Headings — pixel font, rounded onto its grid. */

@layer elements.heading {
    @scope ([data-type="heading"]) {
        :scope {
            font-family: "Press Start 2P", monospace;
            font-weight: 400;
            color: var(--ink-bright);
            text-wrap: pretty;

            /* The pixel font has no descender in the metric sense, hence the line
               height always stands written out here \(vlh K0044\). 1.4 leaves
               exactly half a glyph height between two lines. */
            line-height: 1.55;
        }

        /* NO rounding onto the pixel grid. It was intended here and is wrong
           here: the design sets 28, 22 and 13 px, and round\(\) shifts each
           of these values to the next multiple — 27.5 would become 24, 13
           would become 16. Where the design sets a size, it holds; the
           rounding stays reserved for what we choose ourselves (vlh K0059).

           The ladder hits the values almost exactly anyway: 27.5 against 28,
           21.6 against 22, 13.4 against 13. */
        :scope:is(h1) { font-size: var(--text-head); }
        :scope:is(h2) { font-size: var(--text-head); }
        :scope:is(h3) { font-size: var(--text-large); }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: A button that carries only a symbol. */

@layer elements.icon_button {
    @scope ([data-type="icon-button"]) {
        :scope {
            display: grid;
            place-items: center;
            inline-size: var(--space-open);
            aspect-ratio: 1;
            background: none;
            border: 0;
            color: var(--ink-quiet);
            font-size: var(--text-large);
            line-height: 1;
            cursor: pointer;
        }

        /* No face, no frame: the colour is the whole feedback. A button
           without a face that got one on pointing would jump — and the
           action colour says the same without touching the layout. */
        :scope:where(:hover, :focus-visible) { color: var(--link); }

        /* **The × in the head of a sheet** (vlh T0084, Owner on 11 September
           2026: »der schliessen button im popover für download und für share
           entspricht nicht dem was im item popover zu sehen ist«).

           It is twice as large as an ordinary symbol, because it stands as
           the only control in a head line that otherwise carries only text
           — and because a sheet must let itself be closed before one looks
           for where.

           Golden on pointing and not green: green is the action colour, and
           closing is no action but its end. The colour of the sheet frame
           says »this sheet« instead.

           The measures stood in the item sheet and nowhere else, which is
           why the map sheets carried a smaller ×. Here they stand once, and
           every sheet gets the same; **where** it sits, the sheet still
           decides. */
        :scope[data-role="close"] {
            inline-size: calc(var(--space-open) * 2);
            font-size: calc(var(--text-large) * 2);
        }

        :scope[data-role="close"]:where(:hover, :focus-visible) {
            color: var(--gold);
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: Links — one place where colour, focus and state are fixed. */

@layer elements.link {
    @scope ([data-type="link"]) {
        :scope {
            color: var(--link);
            text-decoration: none;
        }

        /* The focus ring stands here and only here. That is the real gain of
           the element: one place instead of four, and no chance that one of
           them is forgotten. :focus-visible instead of :focus, so that it
           does not appear with the mouse — whoever clicks sees where they
           clicked. */
        :scope:where(:focus-visible) {
            outline: 2px solid var(--link);
            outline-offset: var(--space-tight);
        }

        :scope:where(:hover) {
            text-decoration: underline;
            text-underline-offset: var(--space-tight);
        }

        /* The current page is read from the state, not from a class:
           aria-current tells the screen reader anyway, and a second carrier
           of the same statement could differ from it \(vlh K0005\). */
        :scope[aria-current] {
            text-decoration: underline;
            text-underline-offset: var(--space-tight);
        }

        /* The chevron moves on hovering, not the whole link: a movement through
           transform that does not touch the layout \(vlh K0036\). */
        > span {
            display: inline-block;
            margin-inline-start: 0.35em;
            transition: transform 120ms ease-out;
        }

        :scope:where(:hover, :focus-visible) > span {
            transform: translateX(0.15em);
        }

        @media (prefers-reduced-motion: reduce) {
            > span { transition: none; }
            :scope:where(:hover, :focus-visible) > span { transform: none; }
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The symbol of the mark — face plus mask, both colours from the theme. */

@layer elements.logo_picture {
    @scope ([data-type="logo-picture"]) {
        :scope {
            display: grid;
            place-items: center;
            inline-size: 1.875rem;
            aspect-ratio: 1;
            background: var(--brand);

            /* The same edge as the start button, only narrower: one and a half
               pixels. It turns the field into an object instead of a colour
               area. Measured from the design. */
            box-shadow:
                inset  1.5px  1.5px 0 oklch(from var(--brand) calc(l + 0.14) c h),
                inset -1.5px -1.5px 0 oklch(from var(--brand) calc(l - 0.16) c h);
        }

        /* The signet as a mask on a face in the base colour. No extra node,
           and a theme change alters two variables. */
        /* 62 percent. The value was never the problem — the symbol's viewBox
           was: it lay on the advance instead of on the ink, and the last 125
           units of that are the distance to the next letter. The symbol so
           sat on the left in the field, and enlarging covered the error
           instead of fixing it. Since the viewBox lies on the ink, it sits
           centred, and 62 percent give it air again. */
        :scope::before {
            content: "";
            inline-size: 62%;
            aspect-ratio: 1;
            background: var(--ground);
            mask-image: url("/brand/signet.svg");
            mask-size: contain;
            mask-repeat: no-repeat;
            mask-position: center;
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The word mark. */

@layer elements.logo_text {
    @scope ([data-type="logo-text"]) {
        :scope {
            font-family: "Press Start 2P", monospace;
            font-weight: 400;
            font-size: 0.875rem;
            line-height: 1;
            letter-spacing: 0.04em;
            color: var(--ink);
        }

        /* b and i carry no emphasis here but markup without a voice — exactly
           what the two elements are meant for since HTML5. The italic is
           taken back, because the pixel font has none and the browser would
           otherwise slant it. */
        > i {
            font-style: normal;
            color: var(--brand);
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: Running text. */

@layer elements.prose {
    @scope ([data-type="prose"]) {
        :scope {
            font-size: var(--text);
            line-height: 1.6;
            color: var(--ink);

            /* In ch, not in rem: the line length is a question of characters per
               line and not of width in pixels. 66 is the upper edge of what
               still reads comfortably. */
            max-inline-size: 58ch;
            text-wrap: pretty;
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The legal notice — quiet, but readable. */

@layer widgets.footer_legal {
    @scope ([data-type="footer-legal"]) {
        :scope {
            display: grid;
            gap: var(--space-tight);
            justify-items: center;
            text-align: center;

            /* 80 characters — the width at which a line is still taken in at one
               glance. Measured in ch and not in rem: the limit is a number
               of characters, no length, and it stays right when somebody
               sets the type larger. */
            max-inline-size: 80ch;
            margin-inline: auto;

            /* Spacing upwards: the notice belongs to the footer but does not stand
               on one level with name and game list. */
            margin-block-start: var(--space-open);
        }

        /* Smaller than the running text, but not smaller than what still reads
           comfortably. A legal notice nobody can decipher does not serve
           its purpose — it should be quiet, not hidden. */
        :scope :where([data-type="prose"], small) {
            font-size: var(--text-small);
            color: var(--ink-quiet);
            max-inline-size: none;
        }

        > p { margin: 0; }

        /* The compulsory links stand above the notice and side by side: they
           are two targets and no list, and a ul with two entries is an
           enumeration that is none \(AW2\). The spacing separates them from
           the sentences above without drawing a line. */
        [data-part="links"] {
            display: flex;
            gap: var(--space-open);
            justify-content: center;
            flex-wrap: wrap;
            margin-block-start: var(--space-tight);
        }

        /* Tabular figures, so that the year does not flicker.

           **No more capitals.** They stood here as long as the line was a
           signature — Vioxide // Vivian Voss // Anno MMXXVI. The owner set
           the spelling themselves on 9 September 2026 \(vlh T0077\), and the
           display does not rewrite it: a legal notice shows what stands
           there. The marks carry their capitals in the text \(VGA.RUN,
           VIOXIDE SARL\), the name of the person beside them does not.
           Incidentally, a screen reader often reads continuous capitals as
           an abbreviation. */
        small {
            font-family: ui-monospace, monospace;
            font-variant-numeric: tabular-nums;
            letter-spacing: 0.02em;
        }

        /* The carrier line stands under the notice and quieter than it: it
           names the company, the complete statement stands in the imprint.
           One step smaller suffices for that — below begins what nobody
           reads any more, and a notice nobody deciphers does not serve its
           purpose. */
        [data-part="carrier"] small {
            font-size: 0.8125rem;
            letter-spacing: 0.01em;
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The stage bar — stripes across the full width, title on them. */

@layer widgets.header_banner {
    @scope ([data-type="header-banner"]) {
        :scope {
            display: grid;
            grid-template-columns: 1fr;
            align-items: start;
            gap: var(--space-close);
            padding-block: 2.125rem 1.875rem;
            padding-inline: max(var(--gutter), (100% - var(--measure)) / 2);
            border-block-end: 1px solid oklch(30% 0.014 260);
            color: var(--stage-ink);

            /* The stripes come from a repeated gradient, not from a picture: no
               request, no file, and the colours come from the theme. A
               second game changes three variables and gets its own bar.

               The gradient stands in the theme and not here: it is what
               changes per game. The bar itself stays the same form. */
            background: var(--stage-scan), var(--stage-bar);
            background-size: 100% 3px, auto;
            background-repeat: repeat, no-repeat;
        }

        /* The title stands on the stripes and needs a hold against them. A
           shadow in the deepest colour of the bar itself, not in black: it
           so sits in the same family and acts as an edge, not as dirt. */
        /* 44 px — the largest type of the site and the only one not on the
           ladder. It is the stage itself: the game name, and it should fill
           the bar, not stand in it. Measured from the design.

           The shadow is hard and offset, three pixels to the bottom right —
           no soft glow. That is how screens of that time cast their type,
           and it is the same reason the bar carries a scanline grid. */
        [data-type="heading"] {
            /* An h1 on the portal ({K0002}); its own margin goes, as vlh's p
               had none. */
            margin: 0;
            font-size: var(--text-stage);
            line-height: 1.2;
            color: oklch(99% 0.01 90);
            text-shadow: 3px 3px 0 oklch(22% 0.06 34);
        }

        /* The date line: 13 px, widely tracked, in the light stage colour. The
           chip brings its own size and is overridden here, because it
           stands on the bar and not in the text. */
        [data-type="chip"] {
            font-size: 0.8125rem;
            letter-spacing: 0.12em;
            color: oklch(97% 0.02 70);
            padding-block-end: 0.5rem;
        }

        /* The expansion, under the name. Smaller and set back: it says which
           content holds, and that is information and no announcement. The
           type cut stays the same as the title's — it is the same thing,
           only one level deeper.

           No grid place of its own: the line stands in the flow under the
           title and is conditional. A game without an expansion does not
           show it, and the bar may not jump because of that.

           On the portal the second line is a sentence, not an expansion's
           name ({K0002}): it wraps over two or three lines, so it has the
           line height of running text, 1.35 where vlh has 1.1, and a line
           length an eye can follow. */
        [data-type="edition"] {
            margin: 0;
            font-size: var(--text-head);
            line-height: 1.35;
            max-inline-size: 48ch;
            letter-spacing: 0.02em;
            color: oklch(88% 0.06 70);
        }

        /* From tablet on the note stands beside the title instead of under
           it. Only the difference, and only upwards — the base is the
           narrow form. */
        /* From tablet on two columns: title and expansion one under the other
           on the left, the note on the right. The expansion belongs under
           the name and not beside it — it says which content holds, and
           one reads that as a continuation of the title, not as a second
           statement at the other end.

           Named areas instead of counting: with three children and two
           columns the third otherwise lands in a place nobody chose —
           exactly that happened here. */
        @media (min-width: 35rem) {
            :scope {
                grid-template-columns: 1fr auto;
                grid-template-areas:
                    "title   note"
                    "edition note";
                align-items: end;
            }
            [data-type="heading"] { grid-area: title; }
            [data-type="edition"] { grid-area: edition; }
            [data-type="chip"]    { grid-area: note; }
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: Symbol and word mark side by side. */

@layer widgets.header_logo {
    @scope ([data-type="header-logo"]) {
        :scope {
            display: grid;
            grid-auto-flow: column;
            align-items: center;
            gap: 0.75rem;
            text-decoration: none;

            /* The link does not fill its cell — it is as wide as its content.
               Without that the stretch of the head grid would come through
               and the clickable area would reach up to the navigation. */
            justify-self: start;
        }

        :scope:where(:focus-visible) {
            outline: 2px solid var(--link);
            outline-offset: var(--space-tight);
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The navigation — a switch on the phone, an open list from screen on. */

@layer widgets.header_menu {
    @scope ([data-type="header-menu"]) {
        /* The nav passes its two children through to the head grid instead of
           enclosing them. Otherwise switch and list would have to share one
           area, and the switch would stand on the phone where the list
           begins from screen on — so in the middle instead of outside. */
        :scope { display: contents; }

        /* The button is no bare <button> any more since the element
           icon_button. The old selector no longer applied, and the rule
           hiding it from screen on ran into nothing: it stood on the desk
           and made the head bar taller. The picture comparison found it,
           not the markup — there stands a button that may stand. */
        [data-type="icon-button"] {
            grid-area: toggle;
        }

        /* No display here: a closed popover is display:none, and any statement
           at this point holds it open. The grid therefore stands only in
           the opened state and from screen on, where it lies in the flow
           anyway. */
        > ul {
            grid-area: menu;
            gap: var(--space-close);
        }

        > ul:popover-open {
            display: grid;
        }

        /* On the phone the list stands as a popover over the content. The
           browser brings focus trap, Escape and closing on a click beside
           it — everything that would otherwise be written as script. */
        > ul:where([popover]) {
            margin: 0;
            padding: var(--space-open);
            background: var(--ground-up);
            border: 1px solid var(--surface-4);
        }

        /* Colour, focus ring and state of the link stand in the link element
           and not here — one place instead of four \(vlh K0054\). The widget
           only decides how the links are arranged, and overrides the colour
           for the head line, where it sits more muted than in the running text. */
        /* The typography hangs on the entry, not on the link: a tab without a
           target (span, »soon«) carries the same type, the same capitals and
           the same tracking as one with — it differs only in colour and
           pointer. Before, it hung on the link, and »Bauplan« and »Spiele«
           fell back to the base type (Owner, 3 September 2026, T0026 QA). */
        li {
            font-family: ui-monospace, monospace;
            font-size: var(--text-small);
            letter-spacing: 0.08em;
            text-transform: uppercase;
        }

        [data-type="link"]:not([aria-current]) { color: var(--ink-quiet); }
        [data-type="link"]:where(:hover, :focus-visible) { color: var(--ink); }

        /* A tab whose page does not exist yet. It stands in the flow like the
           others, but accepts no pointer and is quieter — it shows what is
           coming without promising a target (vlh K0079). */
        [data-state="soon"] { color: var(--ink-quiet); cursor: default; }

        /* From screen on the two areas swap roles. The popover is set visible
           and so loses everything popover-like: it stands in the flow, in
           the area menu, as a row. The switch disappears.

           position:static is needed because the default stylesheet gives a
           popover position:fixed. Nothing more: a popover enters the top
           layer only on opening, and this one is never opened — the switch
           no longer stands from screen on. */
        @media (min-width: 60rem) {
            [data-type="icon-button"] { display: none; }

            > ul:where([popover]) {
                display: grid;
                position: static;
                grid-auto-flow: column;
                gap: var(--space-open);
                padding: 0;
                background: none;
                border: 0;
            }
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The language choice — the current value visible, the others unfoldable. */

@layer widgets.header_options {
    @scope ([data-type="header-options"]) {
        :scope {
            position: relative;
            justify-self: end;
        }

        > summary {
            display: grid;
            place-items: center;
            min-inline-size: var(--space-wide);
            padding-block: var(--space-tight);
            padding-inline: var(--space-snug);
            border: 1px solid var(--surface-4);
            font-family: ui-monospace, monospace;
            font-size: var(--text-small);
            letter-spacing: 0.08em;
            text-transform: uppercase;
            color: var(--ink-quiet);
            cursor: pointer;
            list-style: none;
        }

        > summary::-webkit-details-marker { display: none; }

        /* The list hangs over the content instead of pushing it. Anchor
           positioning would be the finer means here, but absolute position
           on a relatively positioned parent costs nothing and is everywhere.
           The change pays only once the list can hit an edge. */
        > ul {
            position: absolute;
            inset-inline-end: 0;
            inset-block-start: calc(100% + var(--space-tight));
            z-index: 10;
            display: grid;
            gap: var(--space-tight);
            min-inline-size: max-content;
            padding: var(--space-snug);
            background: var(--ground-up);
            border: 1px solid var(--surface-4);
        }

        /* The link brings focus ring and state \(vlh K0054\); here stands only
           the more muted colour of the head line. */
        [data-type="link"] { font-size: var(--text-small); color: var(--ink-quiet); }
        [data-type="link"]:where(:hover, :focus-visible) { color: var(--ink); }
        [data-type="link"][aria-current] { color: var(--link); }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The game list — one line with separator dots. */

@layer widgets.page_footer_nav {
    @scope ([data-type="page-footer-nav"]) {
        ul {
            display: flex;
            flex-wrap: wrap;
            gap: var(--space-close);
            font-family: ui-monospace, monospace;
            font-size: 0.625rem;
            letter-spacing: 0.1em;
            text-transform: uppercase;
        }

        /* Flex, not grid: the list is a single stack that wraps, and exactly
           for that flex is the exception that stays allowed. A grid would
           have to know the column count, which depends on the text here. */

        li { color: var(--ink-quiet); }

        /* The separator dot does not belong in the text but between the entries
           — otherwise a screen reader reads it along. */
        li + li::before {
            content: "·";
            margin-inline-end: var(--space-close);
            color: var(--ink-quiet);
        }

        /* The link brings focus and state; here stands only that it sits more
           muted in the footer \(vlh K0054\). */
        [data-type="link"] { color: var(--ink-quiet); }
        [data-type="link"]:where(:hover, :focus-visible) { color: var(--link); }

        /* A coming game is no link and does not look like one either. */
        span { color: var(--ink-quiet); }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: Entry cards — landscape on the phone, portrait from screen on. */

@layer widgets.tool_card {
    @scope ([data-type="tool-card"]) {
        /* On the phone one row: icon left, title and short text beside it,
           the chevron right. From screen on portrait with the long text and
           the way below. Only the areas change — that is the whole rebuild
           \(vlh K0009\). */
        :scope {
            position: relative;
            display: grid;
            grid-template-areas:
                "icon title act"
                "icon text  act";
            grid-template-columns: auto 1fr auto;
            align-items: center;
            column-gap: 0.875rem;
            row-gap: 0.25rem;
            padding: 0.875rem 1rem;
            background: var(--surface-4);

            /* The same pixel edge as button and mark, only in the surface tones:
               two inner shadows instead of a frame. A frame would lie
               outside and shift the face by two pixels; the edge lies inside
               and leaves the card where it is. Measured from the design. */
            box-shadow:
                inset  2px  2px 0 oklch(33% 0.014 var(--hue-surface)),
                inset -2px -2px 0 oklch(12% 0.010 var(--hue-surface));
        }

        /* 48 px, measured from the design — not the element's standard size,
           because the card gives it more weight than a list. */
        /* On the phone 32 px, from screen on 48: in the row the icon is a mark
           beside the title, in the portrait card the stage above it. */
        [data-type="game-icon"] { grid-area: icon; inline-size: 2rem; }
        [data-type="heading"]   { grid-area: title; }
        /* 14.5 px and one step more muted than the page's running text: in the
           card the description is trimmings to the title, not the text one
           came for. Both measured from the design; 14.5 lies between two
           steps of the ladder, and AW22 sides with the design. */
        [data-type="prose"] {
            grid-area: text;
            font-size: 0.90625rem;
            line-height: 1.6;
            color: oklch(65% 0.008 var(--hue-surface));
            max-inline-size: none;
        }

        /* The long text stands only from screen on, the short one only below. */
        [data-type="prose"]:nth-of-type(2) { display: none; }

        /* The heading carries 13 px here, not the size of its level: in the
           card it is a label and no announcement. Measured from the design
           \(vlh K0058\). */
        [data-type="heading"] {
            font-size: var(--text-label);
            text-transform: uppercase;
        }

        [data-type="link"] {
            grid-area: act;
            justify-self: end;
            font-family: ui-monospace, monospace;
            font-size: 0.6875rem;
            letter-spacing: 0.1em;
            text-transform: uppercase;

            /* Pushed down so that the way sits on the same line on all cards,
               however long the description. */
        }

        /* On the phone the link carries only its chevron: the wording stands in
           the card above, and reading the same twice is no service. */
        [data-type="link"] { font-size: 0; }
        [data-type="link"] > span { font-size: 1rem; margin-inline-start: 0; }

        /* The link stretches its clickable area over the whole card. That is
           the reason for position:relative above — and the reason why title
           and icon are not linked themselves. */
        [data-type="link"]::after {

        /* The call of a card whose page does not exist yet. It stands in the
           same place as the link so that the cards stay equally tall — only
           quieter and without a pointer (vlh K0079). */
        [data-state="soon"] { color: var(--ink-quiet); cursor: default; }
            content: "";
            position: absolute;
            inset: 0;
        }

        /* On hovering the card is pressed: the edge turns round, dark top left
           and light bottom right, as with the input field \(vlh K0062\). In
           addition the face takes on a touch of the link colour — six
           percent, just enough to notice without being able to name it.

           No coloured frame: a frame says „selected", and selected the card
           is not. It is pressed, and that is an action, no property. */
        :scope:has([data-type="link"]:where(:hover, :focus-visible)) {
            background: color-mix(in oklab, var(--surface-4) 94%, var(--link));
            box-shadow:
                inset  2px  2px 0 oklch(12% 0.010 var(--hue-surface)),
                inset -2px -2px 0 oklch(33% 0.014 var(--hue-surface));
        }

        /* The transition runs only over the face; the edge jumps. A shadow
           that turns soft would look like glowing instead of pressure. */
        :scope {
            transition: background 90ms ease-out;
        }

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

        /* Portrait from screen on: the icon moves above the title, the card
           becomes one column. Only the areas change — that is the whole
           rebuild \(vlh K0009\). */
        @media (min-width: 60rem) {
            :scope {
                grid-template-areas:
                    "icon"
                    "title"
                    "text"
                    "act";
                grid-template-columns: 1fr;
                grid-template-rows: auto auto auto 1fr;
                align-items: start;
                gap: 1rem;
                padding: 1.625rem;
            }

            [data-type="game-icon"] { inline-size: 3rem; }

            [data-type="prose"]:nth-of-type(1) { display: none; }
            [data-type="prose"]:nth-of-type(2) { display: block; }

            [data-type="link"] {
                justify-self: start;
                font-size: var(--text-micro);
                margin-block-start: auto;
            }

            [data-type="link"] > span { font-size: inherit; margin-inline-start: 0.35em; }
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The footer band — one surface step deeper than the content above. */

@layer slices.page_footer {
    @scope ([data-slice="page-footer"]) {
        :scope {
            padding-block: 1.5rem;
            row-gap: 1rem;
            border-block-start: 1px solid oklch(26% 0.014 var(--hue-surface));

            /* The face changes, not a border: bands separate over the ground, not
               over lines \(vlh K0031\). One step deeper than the content
               above, so that the footer steps back without vanishing. */
            background: var(--surface-1);
        }

        /* Links in the footer carry the footer colour, not the link colour.
           --link is green and means »leads away«; in the content that is an
           offer, in the footer it would be a shout. The footer is the
           quietest band of the page, and two compulsory links in it are no
           invitation but information \(vlh T0077 QA\). They stay recognisable
           by the underline and by lightening on hovering. */
        /* The notice in the size of the rest of the footer (vlh T0077, Owner on
           9 September 2026). It stood one step larger, because it once was
           the signature; now it is the one line that stands alone centred
           in the map view, and there it should fall into line and not
           stand out. */
        [data-type="footer-legal"] small {
            font-size: 0.65625rem;
            letter-spacing: 0.08em;
        }

        a[data-type="link"] {
            color: var(--ink-quiet);
            font-family: ui-monospace, monospace;
            font-size: 0.65625rem;
            letter-spacing: 0.08em;
            text-transform: uppercase;
            /* Underlined, because they would otherwise be recognisable as links
               by nothing any more: the colour is now the footer's, and
               colour alone may not be it anyway \(AW3, WCAG 1.4.1\). The
               spacing keeps the line clear of the capitals. */
            text-decoration: underline;
            text-underline-offset: 0.3em;
            text-decoration-thickness: from-font;
        }

        a[data-type="link"]:where(:hover, :focus-visible) {
            color: var(--ink);
        }

        [data-type="prose"] {
            font-family: ui-monospace, monospace;
            font-size: 0.65625rem;
            letter-spacing: 0.08em;
            text-transform: uppercase;
            color: var(--ink-quiet);
            max-inline-size: none;
        }

        /* The donate button stands under the name and takes only its own
           width: stretched it would be a white bar across the whole band
           instead of a button (vlh T0134). */
        > [data-type="donate-button"] {
            justify-self: start;
        }

        /* From tablet on name and game list stand side by side, the donate
           button under the name, the legal notice below across the whole
           width — it is the quietest line and should not fight with the
           navigation over the same room. */
        @media (min-width: 35rem) {
            :scope {
                grid-template-areas:
                    "name   games"
                    "donate ."
                    "legal  legal";
                /* The gutter is computed, not given as 1fr: beside an inner 1fr three
                   flexible tracks would otherwise share the room and push the
                   content to the middle instead of the edges. */
                grid-template-columns:
                    [full-start] var(--edge)
                    [content-start] auto var(--space-wide) 1fr
                    [content-end] var(--edge)
                    [full-end];
            }

            > [data-type="prose"]            { grid-column: 2; grid-row: 1; }
            > [data-type="page-footer-nav"]  { grid-column: 4; grid-row: 1; justify-self: end; }
            > [data-type="donate-button"]    { grid-column: 2; grid-row: 2; }
            > [data-type="footer-legal"]     { grid-column: 2 / 5; grid-row: 3; }
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The head — the row of mark, targets and language, below it the stage. */

@layer slices.page_header {
    @scope ([data-slice="page-header"]) {
        /* Two rows, four areas. The targets stand in the middle and are
           centred there — not by space-between but by a 1fr track between
           two auto tracks. The difference counts: with space-between the
           middle moves as soon as the ends differ in width, and exactly
           that happens when the language choice changes from DE to Русский.

           The head carries its own ground and dividing line: it sets itself
           off from the page before the bar below begins. Measured from the design. */
        /* **The column grid stands once and is read twice** (vlh T0095): the
           head needs it for the stage, the sticking bar for its own areas.
           Two copies of the same seven tracks would be the sort of
           repetition where the second differs some day (AW11). A custom
           property may carry named grid lines. */
        /* **The head stays put** (Owner, 12 September 2026), and for that it
           has shrunk to what belongs to the controls: mark, targets,
           language, below them the progress bar. The stage stands beside it
           instead of in it.

           The sticking hangs on that. `position: sticky` holds only within
           the containing block, and as long as the stage lay in the head,
           that was the head itself — the bar stood, measured after 787
           points of scrolling, at -787. Now the containing block is the
           `<body>`, and that reaches as far as the page. */
        :scope {
            position: sticky;
            inset-block-start: 0;
            /* Above everything in the band; the map sets its markers to 5. */
            z-index: 6;

            grid-template-areas:
                ".    logo badge menu options toggle .   "
                "fort fort fort  fort fort    fort   fort";
            grid-template-columns:
                [full-start] var(--edge)
                [content-start] auto auto 1fr auto auto
                [content-end] var(--edge)
                [full-end];
            align-items: center;
            row-gap: 0;
            background: var(--surface-3);
            border-block-end: 1px solid oklch(30% 0.014 260);
        }

        /* The inner room sits on the children of the bar, not on the band: the
           stage bar runs below it across the full width and may not inherit
           it. */
        > :where([data-type="header-logo"], nav, [data-type="header-options"]) {
            padding-block: 1rem;
        }

        /* Mark and language lie in the content track, not at an edge with a
           margin. The difference carries far: --edge computes with 100
           percent, and that resolves on use against the surrounding box —
           with a margin so against the element's grid cell and not against
           the band. On the desk the mark therefore stuck at 40 px while the
           content below began at 240. The same trap as a derivation in
           :root, which is computed once there and comes out the same
           everywhere. */
        [data-type="header-logo"]    { grid-area: logo; }

        /* The spacing stands here and not as column-gap on the band: a gap
           between all tracks would push the language choice off the edge
           too. The measure is that of the mark itself, so that symbol, word
           mark and beta box stand in one rhythm. */
        /* No padding-block like the neighbours: the box is the visible face
           itself, and a padding would let it grow instead of its cell. The
           line height comes from the mark beside it, align-items sets it in. */
        [data-type="beta-badge"]     { grid-area: badge; margin-inline-start: 0.75rem; }
        [data-type="header-options"] { grid-area: options; }

        :scope > nav > button { grid-area: toggle; }
        :scope > nav > ul     { grid-area: menu; }

        /* The reading progress (vlh T0095, Owner on 12 September 2026): a strip
           between the target row and the stage that grows along while one
           reads.

           **The resting state is empty, not full.** That is the whole
           precaution: `animation-timeline: scroll()` is not Baseline —
           Chrome and Edge since 115, Safari since 26, Firefox stable keeps
           it behind a flag to this day. Where it is missing, the animation
           does not run, and the element stays as it stands here: squashed
           to zero and so invisible. A bar that stood full without support
           would be worse than none — it would claim something.

           `scale` and not `inline-size`: the one runs on the compositor,
           the other through the layout — and that at every scroll step
           (AW18). `transform-origin` at the start of the line, so that it
           grows from the left; `inset-inline-start` would be the
           alternative and would go through the layout again. */
        [data-part="progress"] {
            grid-area: fort;
            /* The primary colour, the same as the menu item one line above. The
               bar carried a second green for one day, and the owner saw it
               at once — meanwhile there is no second one any more (vlh T0096). */
            background: var(--link);
            transform-origin: 0 50%;
            scale: 0 1;

            /* **Without support no height either.** Squashed to zero the strip
               would be invisible, but its grid row still five points tall —
               in Firefox the head would then stand five points lower than
               elsewhere, for nothing. The height therefore comes only with
               the feature, measured: Firefox 0 px, Chromium 5 px. */
            block-size: 0;
        }

        /* **No retreat at `prefers-reduced-motion`**, and that is a deliberate
           reading of AW18: the rule means movement that happens **without
           doing anything** — parallax, autoplay, transitions. This bar
           moves exclusively in sync with the reader's scrolling and so in
           the same category as the scrollbar beside it. Switching it off
           would take information away and protect from nothing. If the
           owner sees it differently, it is one line.

           The query stands here anyway — it switches the **transitions**
           off, should the strip ever grow one. */
        @supports (animation-timeline: scroll()) {
            [data-part="progress"] {
                /* Five points tall, and that is the allowed exception from AW16: a
                   line whose measure is its appearance. */
                block-size: 5px;
                animation: reading-progress linear;
                animation-timeline: scroll(root block);
            }
        }

        @media (prefers-reduced-motion: reduce) {
            [data-part="progress"] { transition: none; }
        }

        @media (min-width: 60rem) {
            :scope > nav > ul { justify-self: center; }
        }
    }
}

/* The squashing as a picture: from zero to whole, linear over the scroll
   way. A `@keyframes` knows no scope — it therefore stands beside it, in
   the same layer, and carries the package name in its own name, so that it
   does not collide with a foreign one (vlh T0095). */
@layer slices.page_header {
    @keyframes reading-progress {
        to { scale: 1 1; }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The games band — one accordion per game, the label a picture bar.

   The bar is vlh.vga.run's region bar (site/packages/slices/slice_items, the
   region block and the bar), copied with its layers, its edge and its pixel
   type ({K0002}); the picture is the hero's ship instead of a biome. The deck
   inside keeps the columns slice_tools gave it on the portal: one, two with
   the third across both, three. */

@layer slices.slice_games {
    @scope ([data-slice="games"]) {
        /* A ground of its own, one step below the page, with an edge upwards -
           the tool band's, from the design. */
        :scope {
            padding-block: 2.5rem;
            background: var(--surface-1);
            border-block-start: 1px solid oklch(30% 0.014 var(--hue-surface));
        }

        [data-part="game"] + [data-part="game"] { margin-block-start: 1.25rem; }

        /* ---- The bar ------------------------------------------------------ */

        /* The direct child only, as on vlh: a summary further down would
           otherwise carry the bar too. */
        [data-part="game"] > summary {
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 1rem;
            /* The region bar's measure: the aspect ratio of vlh's biome strips,
               bound to its track, a small minimum height (vlh C0021). */
            aspect-ratio: 4717 / 646;
            max-inline-size: 100%;
            min-block-size: 4.5rem;
            padding: 0 1.25rem;
            cursor: pointer;
            overflow: hidden;
            color: var(--ink-bright);

            /* The text carrier, the region bar's: dark at both edges, the
               middle free for the picture. */
            --bar-carrier: linear-gradient(90deg,
                oklch(10% 0.01 260 / 0.88) 0%,
                oklch(10% 0.01 260 / 0.62) 22%,
                oklch(10% 0.01 260 / 0.30) 42%,
                oklch(10% 0.01 260 / 0.30) 58%,
                oklch(10% 0.01 260 / 0.62) 78%,
                oklch(10% 0.01 260 / 0.88) 100%);

            background-color: var(--surface-2);
            background-size: var(--interlace-size), auto, cover;
            background-repeat: repeat, no-repeat, no-repeat;
            /* The ship and its crew sit on the right of the picture, as in
               vlh's hero; centred, a narrow bar would show only water. */
            background-position: center, center, right center;

            box-shadow:
                inset  2px  2px 0 oklch(33% 0.014 var(--hue-surface)),
                inset -2px -2px 0 oklch(12% 0.010 var(--hue-surface));
        }

        /* The picture per game, in the three widths vlh's hero loads: 50 kB on
           the phone, 190 on the desk. */
        [data-game="valheim"] > summary {
            background-image:
                var(--interlace),
                var(--bar-carrier),
                image-set(url("/press/skepp-860.avif") type("image/avif"),
                          url("/press/skepp-860.webp") type("image/webp"));

            @media (min-width: 35rem) {
                background-image:
                    var(--interlace),
                    var(--bar-carrier),
                    image-set(url("/press/skepp-1280.avif") type("image/avif"),
                              url("/press/skepp-1280.webp") type("image/webp"));
            }

            @media (min-width: 60rem) {
                background-image:
                    var(--interlace),
                    var(--bar-carrier),
                    image-set(url("/press/skepp-1920.avif") type("image/avif"),
                              url("/press/skepp-1920.webp") type("image/webp"));
            }
        }

        /* The game's name in the pixel face, the region name's size: it is the
           heading of the block. */
        [data-part="game-name"] {
            font-family: "Press Start 2P", monospace;
            font-size: var(--text-large);
            line-height: 1.55;
            color: var(--ink-bright);
            text-shadow: 0 1px 3px oklch(0% 0 0 / 0.8);
        }

        /* Version and expansion on the right, in the place and type of vlh's
           count: white like the name, both stand on the same strip. On the
           phone the expansion drops and the version stays, as vlh's count
           drops its word - mobile first, the expansion added from 30 rem. */
        [data-part="game-note"] {
            margin-inline-start: auto;
            font-family: "Press Start 2P", monospace;
            font-size: var(--text-large);
            line-height: 1.55;
            color: var(--ink-bright);
            text-shadow: 0 1px 3px oklch(0% 0 0 / 0.8);
            white-space: nowrap;

            i {
                display: none;
                font-style: normal;

                @media (min-width: 30rem) { display: inline; }
            }
        }

        /* ---- The deck ----------------------------------------------------- */

        .deck {
            display: grid;
            gap: 1.25rem;
            margin-block-start: 1.25rem;

            @media (min-width: 35rem) {
                grid-template-columns: repeat(2, minmax(0, 1fr));
            }

            @media (min-width: 60rem) {
                grid-template-columns: repeat(3, minmax(0, 1fr));
            }
        }

        @media (min-width: 35rem) {
            .deck > :nth-child(3) { grid-column: 1 / -1; }
        }

        @media (min-width: 60rem) {
            .deck > :nth-child(3) { grid-column: auto; }
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: The legal pages — readable, unobtrusive, without ornament. */

@layer slices.slice_legal {
    @scope ([data-slice="legal"]) {
        :scope {
            display: grid;
            gap: var(--space-open);

            /* 68 characters. A legal text is read from top to bottom and not
               skimmed; the line may not be longer than the eye carries back
               without a jump. In ch, because the limit is a number of
               characters and no length. */
            max-inline-size: 68ch;
            margin-inline: auto;
            padding-block: var(--space-open);
        }

        > h1 {
            font-size: var(--text-large);
            margin: 0;
        }

        section {
            display: grid;
            gap: var(--space-tight);
        }

        h2 {
            /* `--text-base` does not exist and never did: the heading inherited its
               size, and that is the running text (vlh T0146S6). Now it says so. */
            font-size: var(--text);
            margin: 0;
            color: var(--ink-bright);
        }

        p { margin: 0; }

        /* The lead carries the note on the governing language version and
           therefore stands quieter than the text, but before it. */
        [data-part="intro"] { color: var(--ink-quiet); }

        [data-part="updated"] {
            color: var(--ink-quiet);
            margin-block-start: var(--space-open);
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: What is new here — one column of dated entries.

   vlh.vga.run's slice_news with one column ({K0002}): heading and list stand
   in the slice's content track themselves, so the container vlh needs for
   its two columns falls away. */

@layer slices.slice_news {
    @scope ([data-slice="news"]) {
        :scope {
            padding-block: var(--space-open);
            row-gap: var(--space-close);
            align-content: start;
        }

        /* No `list-style`: the date in front is the mark. */
        [data-part="list"] {
            display: grid;
            gap: var(--space-close);
            margin: 0;
            padding: 0;
            list-style: none;
            max-inline-size: 72ch;
        }

        [data-part="list"] > li {
            display: grid;
            grid-template-columns: auto 1fr;
            gap: var(--space-snug);
            align-items: baseline;
        }

        /* The date in the house's label type - monospace, so that dates
           standing one under the other stand one under the other. */
        [data-part="date"] {
            font-family: ui-monospace, monospace;
            font-size: var(--text-small);
            color: var(--ink-quiet);
            white-space: nowrap;
        }

        [data-part="word"] { color: var(--ink); }

        [data-part="empty"] {
            margin: 0;
            color: var(--ink-quiet);
        }
    }
}

/* Copyright 2026 Vivian Voss. Code by Vivian Voss. Licensed under the BSD-3-Clause.
   SPDX-License-Identifier: BSD-3-Clause
   Scope: the base frame — three regions above one another, the content in one column. */

@layer templates.vga-page {
    @scope ([data-template="vga-page"]) {
        :scope {
            display: grid;
            grid-template-areas: "header" "content" "footer";
            grid-template-rows: auto 1fr auto;
            min-block-size: 100svb;
        }

        /* Assigned through the element, not through a class: the region is a
           property of the frame, and the slice that fills it should not have to
           know of it. A second template arranges the same slices differently by
           changing nothing but the areas. */
        > header { grid-area: header; }
        > main   { grid-area: content; }
        > footer { grid-area: footer; }

        /* The content area is a grid with one column; every row in it is a slice
           and as wide as the page \(vlh K0031\). No gap: slices butt against each
           other, because their surfaces are meant to alternate — the distance
           arises inside the slice as its own inner space, not between two as a
           gap. A gap would let the ground of the page show through and make
           three bands out of two. */
        > main {
            display: grid;
            grid-template-columns: 1fr;

            /* **The slices stand at the top, they do not fill** (vlh T0095). A
               grid without a row definition lays out implicit `auto` rows, and
               `align-content` stands at `normal` there — which here means
               `stretch`: is the content shorter than the page, the rest is
               spread over all rows. `min-block-size: 100svb` on the frame sees
               to it that this rest almost always exists.

               It showed when the stage moved from the head into the content:
               on the building plan page, which carries little, it stood 188
               points high, on the item page 159 — the same widget, the same
               values, only more rest to spread. A building block whose height
               depends on how much stands *beside* it is none. */
            align-content: start;
        }

    }
}

