:root {
    /* Base values only — measureLayout() in script.js overwrites both on the
       container, scaling each independently.

       Columns are near-fixed rather than fluid, which is what stops card copy
       re-wrapping on every pixel of a drag: the old `repeat(6, 1fr)` let a
       column slide anywhere from 169px to 360px. Now a column stretches at most
       MAX_COL_SCALE before the grid takes another column instead, and the
       leftover centres as edge whitespace. The gap has its own MAX_GAP_SCALE
       cap, tuned separately. */
    --col-w: 216px;
    --gap: 16px;

    /* Every S card is exactly this tall, so the 1-column stack is uniform.
       Short content pads out with whitespace rather than shrinking the card. */
    --s-h: 284px;
    /* ...and never wider than this. S is capped rather than stretching, so a
       card never outgrows the design; below the cap it takes what it can get. */
    --s-w: 216px;

    /* The editorial column's *minimum* width — it grows past this to soak up
       slack, but never reads narrower. The grid sits beside it and stays
       column-locked, so the article is the flexible element. --page-gap is the
       space between the two. */
    --side-w: 680px;
    --page-gap: 16px;

    /* Hard cutoff for the whole page. Content never grows wider than this, no
       matter the screen; past it the page centres and the surplus is margin. */
    --page-max: 1920px;
}

/* Load-bearing, not boilerplate. --s-h and the L cards' aspect-ratio both size
   the box, and under the default content-box the padding is added on top: S
   cards came out 336px and 344px tall instead of a uniform --s-h, and "square"
   L cards were square plus 16px. */
*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    margin: 0;
    padding: 40px;
    font-family: sans-serif;
    background-color: #ffffff;
}

/* Page shell, shared by both pages. The grid sits in a flex row so it works
   alone (index.html) and beside an article (blog.html) with no grid changes at
   all. Capped at --page-max (1920px) whether or not there is a side column —
   that is the single content cutoff. */
.page {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--page-gap);
    margin: 0 auto;
    max-width: var(--page-max);
}

/* Editorial column — the flexible half. It reserves --side-w (680px) as its
   floor and grows past it to absorb whatever width the grid's whole columns do
   not use, so a wide screen widens the article rather than opening a gutter.
   min-width, not a fixed width, is what lets measureLayout() reserve a stable
   680 for it while its live width floats. */
.side-content {
    flex: 1 1 var(--side-w);
    min-width: min(var(--side-w), 100%);
    max-width: 100%;
}

.side-media {
    width: 100%;
    aspect-ratio: 16 / 9;
    background: #e8e8e8;
}

.side-content h1 {
    font-size: 24px;
    line-height: 1.25;
    margin: 16px 0 8px;
}

.side-content p {
    font-size: 14px;
    line-height: 1.5;
    color: #444;
    margin: 0 0 12px;
}

/* The grid — the column-locked half. script.js pins its width to exactly the
   columns it holds (`width` set inline by applyLayout), so the leftover falls
   *outside* the wrapper and the article's flex-grow claims it, instead of
   trailing inside the grid. `flex: 0 0 auto` because the pinned width is the
   whole story: it must neither grow nor shrink away from it.

   Because the width is now imposed by JS rather than read back from the wrapper,
   measureLayout() measures `.page` (a stable parent), never this element — so
   there is no risk of the measurement feeding on its own output. The wrap
   threshold falls out of the same widths: once the article's 680 floor plus one
   S column no longer fit, flex-wrap drops the grid onto its own line and it
   takes the full width there.

   No `container-type: inline-size` — it applies containment that sizes the
   element as if empty, which collapsed the wrapper to a sliver in any
   shrink-to-fit context. Nothing here uses container queries. */
.grid-wrapper {
    flex: 0 0 auto;
    min-width: 0;
}

/* Column count and gap are both set by script.js as inline custom properties.
   There are deliberately no media queries here: the tiles are JS-injected, so
   CSS could never lay this grid out on its own anyway, and keeping the column
   maths in one place removes the JS/CSS breakpoint coupling that used to be the
   easiest thing in this project to break.

   `justify-content: start` puts every pixel the columns did not take on the
   right-hand end, rather than splitting it. This is the default (GAP_MODE
   'right' and 'gap-right' in script.js) — centring by default looked wrong: on
   blog.html it opened a gap between the article and the grid's first column
   that read as a broken gutter rather than as deliberate whitespace, and the
   leading sliver did the same on index.html. The grid keeps a hard left edge;
   the slack trails. See the [data-gap-mode="edges"] override below for the
   opt-in centred variant. */
#grid-container {
    display: grid;
    grid-template-columns: repeat(var(--cols, 1), var(--col-w));
    gap: var(--gap);
    justify-content: start;
    /* No hardcoded grid-auto-rows here. Tiles size themselves. */
}

/* 1-column layout is the exception: one S-width column rather than a scaled
   --col-w one. Capped at --s-w, shrinking below it only if there is less room
   than that. */
#grid-container[data-cols="1"] {
    grid-template-columns: min(var(--s-w), 100%);
}

/* GAP_MODE = 'edges' in script.js: split the leftover width between both
   edges instead of trailing it on the right. Columns and gaps are left at
   their base size in this mode (script.js skips the MAX_COL_SCALE /
   MAX_GAP_SCALE stretch), so this is the only rule that changes between it
   and the default. */
#grid-container[data-gap-mode="edges"] {
    justify-content: center;
}

/* Base Tile Styling */
.tile {
    border-radius: 0; /* Sharp corners */
    transition: transform 0.2s ease-in-out;
    background-color: #ffffff;
    overflow: hidden;
    cursor: pointer;
    display: flex;
    flex-direction: column;
    /* Hug content instead of stretching to the row height, so the visible card
       (and its hover target) never includes the empty grid cell below it. */
    align-self: start;
}

.tile:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
    z-index: 1;
}

/* Three card sizes, and only three:
     L — 2 columns wide, square footprint. 2-column layouts and up.
     M — 1 column wide, content-sized height. 2-column layouts and up.
     S — the 1-column layout's only card. Full width, fixed height.
   L and M never appear at 1 column; S never appears above it. */
.tile.l {
    grid-column: span 2;
    aspect-ratio: 1 / 1;
}

.tile.m {
    grid-column: span 1;
}

/* Every S is exactly --s-h tall whatever it holds, so the single-column stack
   is uniform. Content that falls short pads out with whitespace instead of
   shrinking the card. */
.tile.s {
    grid-column: span 1;
    height: var(--s-h);
}

/* Card tiles: light tinted background, image placeholder inset with padding,
   caption underneath */
.tile.card {
    background: var(--bg);
    padding: 8px;
}

.tile.card .media {
    width: 100%;
    aspect-ratio: 1 / 1; /* square placeholder by default (M cards) */
    background: var(--img);
}

/* L and S both have a fixed overall footprint, so their image takes whatever
   space the caption leaves above it. (M has no fixed height, so it keeps the
   square placeholder above and lets the caption flow below.) */
.tile.l.card .media,
.tile.s.card .media {
    flex: 1 1 auto;
    min-height: 0;
    aspect-ratio: auto;
}

/* L SEO cards keep a fixed 16:9 image, with copy filling below it. */
.tile.l.card.wide .media {
    flex: 0 0 auto;
    aspect-ratio: 16 / 9;
}

.tile.card .caption {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px 2px 2px;
}

.tile.card .body {
    font-size: 12px;
    line-height: 1.3;
    color: var(--fg);
    white-space: pre-line;
}

/* L SEO copy reads as a bold coloured heading */
.tile.card.wide .body {
    color: var(--fg);
    font-weight: 700;
    font-size: 13px;
}

/* SEO tiles at M and S: light background, label on top, bold copy below, no
   image. At S the fixed height leaves whitespace under short copy. */
.tile.text {
    background: var(--bg);
    padding: 12px;
}

.tile.text .body {
    margin: 8px 0 0;
    font-size: 15px;
    font-weight: 700;
    line-height: 1.35;
    color: var(--fg);
    white-space: pre-line;
}

/* Shared label styling — labels are not bold; only SEO headings are */
.label {
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.2px;
    color: var(--fg);
}

#sentinel {
    height: 80px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: 20px;
}

.loading-text {
    color: #888;
    font-size: 0.9rem;
    font-weight: bold;
    letter-spacing: 1px;
}

/* No breakpoint media queries. Column count is derived from the container width
   by measureLayout() in script.js and applied as --cols, so the breakpoints
   exist in exactly one place. The old duplicate 400/600/750/900/1100px ladder
   in JS and CSS had to be kept in sync by hand or tiles overflowed. */

/* Dev settings overlay (created by createSettingsOverlay() in script.js) for
   live-tuning the layout/content constants without editing code. Not part of
   the visual design of the grid itself — safe to delete this whole block
   along with its JS section if the overlay is no longer wanted. */
#settings-overlay {
    position: fixed;
    top: 12px;
    right: 12px;
    z-index: 1000;
    font-family: sans-serif;
    font-size: 13px;
}

#settings-toggle {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    border: none;
    background: #222;
    color: #fff;
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}

#settings-body {
    display: none;
    position: absolute;
    top: 44px;
    right: 0;
    width: 260px;
    max-height: 80vh;
    overflow-y: auto;
    background: #fff;
    border: 1px solid #ddd;
    border-radius: 8px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
    padding: 12px;
}

#settings-overlay.open #settings-body {
    display: block;
}

#settings-body h3 {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: #888;
    margin: 16px 0 6px;
}

#settings-body h3:first-child {
    margin-top: 0;
}

.settings-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    margin-bottom: 6px;
}

.settings-row label {
    flex: 1;
    color: #333;
}

.settings-row input,
.settings-row select {
    width: 90px;
}

/* Info button: collapsed by default, toggles the .settings-desc paragraph
   right below it. A click rather than a hover so it also works on touch. */
#settings-body button.settings-info {
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    margin: 0;
    padding: 0;
    border: 1px solid #bbb;
    border-radius: 50%;
    background: #f0f0f0;
    color: #666;
    font-size: 10px;
    font-style: italic;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
}

#settings-body button.settings-info:hover,
#settings-body button.settings-info.active {
    background: #dbe9ff;
    border-color: #8ab4f8;
    color: #1a56c4;
}

/* German explanation, collapsed until its info button is clicked. */
.settings-desc {
    margin: -2px 0 8px;
    padding: 6px 8px;
    background: #f7f7f7;
    border-radius: 4px;
    color: #555;
    font-size: 12px;
    line-height: 1.4;
}

#settings-body button {
    width: 100%;
    margin-top: 8px;
    padding: 6px;
    border: 1px solid #ccc;
    border-radius: 4px;
    background: #f5f5f5;
    cursor: pointer;
}

#settings-body button:hover {
    background: #ececec;
}
