/* ==========================================================================
   Start slice (the landing, #3023). Builds on the btj.css design tokens.
   Mobile-first: everything stacks; the bands become grids and the aside moves
   beside the main column from 62rem. No JS — the strip and the shelves scroll-
   snap instead of the legacy hAccordion + serialScroll pair (anatomy §6.3).
   ========================================================================== */

.start-page { display: grid; gap: var(--sp-6); }

/* Temporary M2 route nav — deleted with the /start route in M4. */
.start-preview {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1) var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    background: var(--c-badge-bg);
    border-radius: 0;
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

.start-title { font-size: var(--fs-2xl); margin: 0; }

/* NO landing draws a visible page heading — the campaigns are the content (2026-08-18; it was Allt
   only until then). The h1 is still in the document: this is the visually-hidden pattern, so the
   heading is there for a screen reader and a crawler and out of flow for everyone else, which is
   why removing it moves the content up without a single margin being retuned. Which shop the reader
   is in is said by the lit media tab and the scoped search field instead — both chrome, both on
   every landing. */
.start-title-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.start-notice {
    margin: 0;
    padding: var(--sp-4) var(--sp-5);
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-left: 4px solid var(--c-border);
    border-radius: 0;
    color: var(--c-text-muted);
}

.start-notice-error { border-left-color: var(--c-danger); color: var(--c-text); }

/* --- main column + aside ------------------------------------------------- */

.start-columns { display: grid; gap: var(--sp-5); }

/* .start-main IS GONE (2026-08-18), and the wrapper is worth a paragraph because it was right for the
   question it answered and wrong for the next one. It existed (2026-08-17) to start the rail level with
   the campaign banner instead of below the whole main column, by making strip+bands one grid child —
   a second column that begins below the first screenful being a third row wearing a column's width.

   THE RULE IT TAUGHT: welding two blocks into one column fixes the edge they share and forfeits the
   edges they do not. Strip and bands in one column tied the ad grid's left edge to the rail's right
   edge, while the ad grid has to line up with the SHELF sections beneath it, which span the page. One
   edge could be satisfied, never both.

   So the two-column row is the strip and the rail alone, and .start-bands spans both columns under
   them. The rail is still level with the banner — by sharing its grid ROW, which is what "beside"
   meant. The wrapper's own note (that a hand-placed aside would need `grid-row: 1 / -1`, and that -1
   cannot mean "all of them" in an implicit grid) is answered the same way: an aside that occupies
   exactly one row needs no span at all. */
/* THE AD GRID STANDS ON THE PAGE'S OWN GROUND (2026-09-11, drawn from the designer's screenshots):
   the white cells are the figure against --c-bg, the same way the shelves below are, so the landing
   is one ground from the strip to the footer rather than a Smultron block between two plain ones.

   NO INLINE PADDING, because a block with no ground has no inside for it to inset: padding without
   a ground is an invisible frame, and it put every cell 16px inside the strip's edge above it and
   the page's edge beside it. The cells start on the page gutter, the line the strip, the search
   field and the shelves start on. The block padding stays — it is the space between the strip and
   the grid, not an inset. */
.start-bands {
    display: grid;
    gap: var(--sp-5);
    padding-block: var(--sp-5);
}

/* A MEDIA LANDING'S TITLES RAIL TAKES THE AD GRID'S ANSWER: it sits beside that grid, so it has no
   ground of its own either — white puffs on --c-bg — and no padding, for the same reason: the first
   puff's top is the strip's top and its right edge is the page's.

   Selected by what is IN it, not by a page flag: the rail that holds a puff is the media landing's,
   and no view model has to carry a variant for the stylesheet to know which of the two it is.

   BOTH DECLARATIONS ARE LOAD-BEARING: .start-aside below gives the start page's shortcuts rail an
   inline padding and a solid --c-accent ground, and this rule is the only thing that keeps them off
   a media landing. Leave either unstated here and the puffs come back inset, or in a burgundy
   frame. */
.start-aside:has(> .start-puff) {
    padding: 0;
    background: none;
}
/* THE RAIL IS THE PAGE'S ONE SOLID REGION — `--c-accent`, i.e. burgundy since 2026-09, filled
   rather than tinted, which is what separates it from the ad grid beside it now that both are in
   the same hue family. The ad grid is white cells on the page's own ground; the rail is a solid
   slab carrying picture tiles. Weight, not temperature.

   THAT IS THE THIRD ANSWER AND THE HISTORY IS THE USEFUL PART. The rail was --c-warm-soft for one
   turn and the warm was refused (asked for, 2026-08-18) with a second reason worth keeping:
   --c-warm-soft was byte-identical to --c-pink, so the rail was exactly the header band, and a
   region that matches the chrome reads as chrome. It then took --c-sage-soft, a pale green
   belonging to no band and no status — and the argument for it was explicitly that "teal and
   orange are both spoken for by controls", which the one-hue palette dissolves. Sage is deleted
   (see btj.css); the rail had already moved to --c-accent before that, and this comment had not
   followed it.

   WHAT TO WATCH, AND IT IS NOT SETTLED IN SOURCE: the tiles inside are the frameless image cells,
   whose `multiply` blend composites against whatever ground they stand on. Their baked-in white
   strips used to drop out against a PALE green; they now composite against a dark burgundy, which
   is a much stronger ground for a blend mode. Nothing here is wrong on paper and it needs a
   browser — photographic tiles are where multiply against a dark magenta-red goes unpleasant. */
.start-aside {
    display: grid;
    /* sp-2, not sp-3: at 10rem the rail's own chrome is a visible share of it — four gaps and two
       paddings were ~60px of a ~500px column, which is a tile's worth of height spent on air. */
    gap: var(--sp-2);
    align-content: start;
    /* INLINE ONLY (asked for, 2026-08-18: the rail's bottom has to be the campaign label's). The
       padding was even on all four sides while the rail was a tinted region, where it was the inset
       of a block. With the ground gone it is invisible slack, and on the block axis it was doing
       active harm: the last banner stopped 8.3px above the slide's bottom edge (measured 336.3
       against 344.6) — two blocks in the same row ending on two different lines, with nothing drawn
       to explain why.

       The inline half stays and is load-bearing: --sp-2 here plus the --sp-4 - --sp-2 margin in the
       62rem block is what puts the tiles' left edge on the page gutter, the same line the ad grid's
       cells and the shelf headings sit on (measured: all four at 73.5).

       It also returns 16px to the height budget, which is why --rail-card goes up in the same turn. */
    padding-block: 0;
    /* --sp-4 inside the burgundy: this rail HAS a ground, so its box edge is the line it shares with
       the page — the gutter the ad grid's cells and the shelves start on — and the padding is the
       inset of its tiles inside that ground, not a frame. */
    padding-inline: var(--sp-4);
    /* THE GROUND IS BACK, AND FLUSH (asked for, 2026-08-18: a highlight behind the rail, but not
       standing over the first tile or under the last). It was taken off a turn earlier for a real
       reason — the tint showed as a green margin down the sides and ends of every campaign banner,
       a frame the artwork did not ask for — and what fixes that is not the colour, it is the padding.

       `padding-block: 0` above is the whole of it: the tint now starts on the top edge of the
       shortcuts card and stops on the bottom edge of the last banner, so it is a ground the tiles sit
       ON rather than a box they sit IN. The inline padding stays --sp-4 (see above).

       --c-accent, the Sök button's own fill (asked for, 2026-08-18: a dark green like the search
       button). The sage and grey reasoning above is kept as the record of what this replaces.

       This is the first DARK region on the landing, and that changes what the ground does. A pale
       tint says "these belong together" and lets the tiles stay the figure; a dark one makes the rail
       the figure and the page its ground — which is the right reading here, because the shortcuts are
       the one block on this page that is navigation rather than merchandising, and the button they
       now echo is the other place a reader goes to leave the page.

       --c-accent and not --c-accent-dark on purpose: the button uses the dark step for HOVER, so a
       region painted in it would be the pressed state of a control nobody can press. */
    background: var(--c-accent);
}
/* --- bands ---------------------------------------------------------------- */

/* A <ul> since #3064 — the cells are a set of peers and the strip and the shelves already say
   so. The list reset is the whole cost: the element is still the grid container and the cells
   are still its direct children, so every selector below is unchanged. */
.start-band { display: grid; gap: var(--sp-4); margin: 0; padding: 0; list-style: none; }

/* THE BANNERS GO IN PAIRS, AND PAIRS ONLY, ON EVERY LANDING (asked for, 2026-09-14). StartLayout
   merges slots 1-3 and 8-10 into this one list in slot order and sends every cell; an odd last
   banner is hidden here rather than dropped on the server, because small banners go in threes
   (below) and whether they are small is only known once the browser has the pictures. Two columns,
   or one where two do not fit. The full-width band and the feature row are not part of it.

   18rem is the width a banner stops being readable under — the cells are editor uploads 600-842px
   across with their own wording inside them, drawn by a rule that is a ceiling and never a target
   (.cell-image img) — so 37rem, two of them and a --sp-4 gutter, is where the second column starts.

   Asked of the AD GRID's width, not the viewport's and not the band's: a band cannot query its own
   width to set its own columns, and the grid's width is not a function of the window — beside the
   15rem titles rail it is ~664px at a 992px window and at most 856px at any. */
.start-bands { container: start-bands / inline-size; }

.band-pairs { grid-template-columns: minmax(0, 1fr); }

@container start-bands (width >= 37rem) {
    .band-pairs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* The unpaired last banner, and a band left with nothing to draw — so it adds no gap either. */
.band-pairs:not(.is-triplets) > :nth-child(odd):last-child { display: none; }
.band-pairs:not(.is-triplets):has(> :only-child) { display: none; }

/* AND SMALL BANNERS GO IN THREES, AND THREES ONLY (asked for, 2026-09-14). start.js adds .is-triplets
   when every banner in the band is narrower than 18rem — /ljudbocker/'s 235x150 uploads today — so a
   pair cell is not mostly empty ground around a picture that will not grow. 47rem is three 15rem cells
   and two --sp-4 gutters: a third of the grid stays at least as wide as those uploads, so threes never
   shrink them.

   A leftover of one or two is hidden the same way pairs hide theirs (asked for: "5 -> one row of 3"):
   the 3n+1th cell when it is last or second to last, and the 3n+2th when it is last. Below 47rem the
   band is back in pairs, so it takes the pairs rule instead — which is why both are keyed on the
   container rather than on the class alone. */
@container start-bands (width >= 47rem) {
    .band-pairs.is-triplets { grid-template-columns: repeat(3, minmax(0, 1fr)); }

    .band-pairs.is-triplets > :nth-child(3n + 1):last-child,
    .band-pairs.is-triplets > :nth-child(3n + 1):nth-last-child(2),
    .band-pairs.is-triplets > :nth-child(3n + 2):last-child { display: none; }

    .band-pairs.is-triplets:not(:has(> :nth-child(3))) { display: none; }
}

@container start-bands (width < 47rem) {
    .band-pairs.is-triplets > :nth-child(odd):last-child { display: none; }
    .band-pairs.is-triplets:has(> :only-child) { display: none; }
}

/* AND A BAND OF ONE IS A BAND OF ONE WHATEVER ITS TRACKS SAY. .band-feature is `3fr 1fr` (or `1fr
   1fr` where every cell is a bare banner), so a cell left alone in it parks in track 1 with the
   other track as bare ground — the same fault as above and not reachable by the same rule, since
   that grid's column count is not the band's width. It is reachable now because start.js drops an
   undersized banner (see there), which is what leaves a two-cell band holding one: /ljudbocker/'s
   feature band pairs a 495 upload with a 235 one. No-op everywhere else — .band-full's cell is
   already the only child of a single column, and an auto-fit band of one collapses its empty
   tracks. */
.start-band > :only-child { grid-column: 1 / -1; }

/* --- ad-area cell --------------------------------------------------------- */

.start-cell {
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 0;
    padding: var(--sp-4);
    overflow-wrap: break-word;
    transition: box-shadow 120ms ease, border-color 120ms ease;
}

/* Lift only on hover: a page of twelve equally shadowed cards reads as noise, and the campaign
   images are what should carry the weight. */
.start-cell:hover { border-color: var(--c-outline); box-shadow: var(--shadow); }

/* A CELL THAT IS ONLY A PICTURE IS NOT A CARD (asked for, 2026-08-18: "frames in the tiles").
   Most ad-area cells on tst carry nothing but the editor's banner — no heading, no text, no link
   list — and the card treatment then draws a white mat and a grey line around an image that already
   has its own edge, its own background and its own wording. That is a frame around a frame, and on
   the indigo ground it is the frame that reads first.

   So: no padding, no line, no ground for those. The banner IS the tile. The rule is written with
   :has() on the *shape of the content* rather than on a class the model would have to emit, because
   the condition is exactly "there is nothing here but the picture" — a cell that gains a heading or a
   link list becomes a card again with no code change. Engines without :has() keep the framed cell,
   which is what shipped until now.

   Hover goes with it: a borderless, groundless tile has nothing to darken or lift, and the image
   already dims on hover (a.cell-image:hover img above). */
.start-cell:has(> .cell-image:only-child) {
    padding: 0;
    border: 0;
    background: none;
}

.start-cell:has(> .cell-image:only-child):hover {
    border-color: transparent;
    box-shadow: none;
}

/* The image box's own tint goes too. It is there to fill the letterbox when `contain` bites inside a
   card; with no card around it, a 750px banner in a wider cell would draw a warm-grey band down each
   side of itself on the indigo ground — the frame removed above, redrawn one element in. */
.start-cell:has(> .cell-image:only-child) > .cell-image { background: none; }

/* AND THE WHITE INSIDE THE PICTURE (reported 2026-08-18). Removing the card does not remove every
   white rectangle, because some of them are painted into the upload: h146301.jpg is 188x55, teal
   #00635e — our own accent — for its top 42 rows and PURE WHITE, full width, for the last 13. Inside
   a white card that strip was invisible; on the indigo ground it is a white bar under the banner, and
   no CSS box property can reach it, since it is image data and a JPEG has no alpha.

   That particular upload is no longer drawn — it was the rail's unlinked, alt-less logo and the view
   stopped rendering it later the same day (AdAreaCellView.ImageIsDecoration). The rule stays, and so
   does the example: baked-in white is a property of editor uploads, not of that one file.

   `multiply` is what reaches it: white multiplied by the ground is the ground, so the baked-in white
   drops out and the banner sits on the tint as if it had been cut out. Nothing is cropped — the rule
   that authored artwork may not lose pixels (see .cell-image above) holds, because a blend does not
   remove anything, it composites. Two consequences worth stating: everything else in the picture is
   multiplied too, which against a near-white ground (the page's own --c-bg, #faf7f6, since
   2026-09-11 — Smultron and indigo before it were near-white too) is a shift of a few
   per cent, and white *text* inside a banner becomes the ground colour rather than white — legible
   where it sits on the campaign's own dark fill, which is where such text is.

   Scoped to the image-only cells: a cell with text keeps its white card, and blending a card onto the
   band would tint the card itself.

   ONE element blends, never both: .cell-image is the <img> itself when there is no IMGLINK and the
   wrapping <a> when there is, so the direct child covers both shapes — putting it on the inner <img>
   as well would composite the picture against the anchor and then the anchor against the page, and
   every non-white pixel would be multiplied twice. */
.start-cell:has(> .cell-image:only-child) > .cell-image { mix-blend-mode: multiply; }

.cell-heading { margin: 0; font-size: var(--fs-lg); line-height: 1.3; }
.band-full .cell-heading { font-size: var(--fs-xl); }
.cell-text { margin: 0; color: var(--c-text-muted); }
.band-feature .cell-text,
.band-pairs .cell-text { font-size: var(--fs-sm); }

/* .cell-image sits on the <img> when the element has no IMGLINK and on the wrapping <a> when it
   has one (_AdAreaCellImage.cshtml), so every rule here has to hold for both shapes: the radius
   needs the clip to reach an inner <img>, and that <img> has to be a block — the base sheet's
   img rule sets no display, and an inline image leaves a baseline gap inside the anchor. */
.cell-image {
    display: block;
    border-radius: 0;
    overflow: hidden;
    /* start, not stretch: as a grid item spanning the cell's rows it would otherwise grow to the
       full cell height and frame the image in empty background. */
    align-self: start;
    background: var(--c-badge-bg);
}

/* Campaign images arrive in whatever size the editor uploaded, and they are *authored* images:
   the campaign's own headline is drawn inside the picture. Fixing the ratio per style and cropping
   to fill kept a row aligned, but on tst the uploads run from 600x383 (≈3:2, the grid puffs) to
   842x314 and 750x250 (the aside banners) — so the 3:2 box cut the ends off the banner text
   ("SOMMARNYTT PÅ ENGELSKA" lost both edges). Nothing an editor wrote may be cropped away, so the
   image keeps its own ratio; the max-height only stops a portrait upload from owning the band, and
   `contain` letterboxes rather than distorts when it bites.

   `width: auto` and not 100%: the uploads are 600-842px across and a stacked aside tile is wider
   than that, so the width rule was ENLARGING a 750px banner to fill it — an authored picture drawn
   above its own resolution, which is the blur on "INLOGGNINGAR" below 62rem. The tile is a ceiling
   now, never a target, and the picture centres in whatever room is left. */
.cell-image img,
img.cell-image {
    display: block;
    width: auto;
    max-width: 100%;
    height: auto;
    max-height: 24rem;
    margin-inline: auto;
    object-fit: contain;
}
a.cell-image:hover img { opacity: 0.92; }

/* Campaign-authored markup: keep it inside the cell whatever it contains. */
.cell-html { max-width: 100%; overflow-x: auto; }
.cell-html img { max-width: 100%; height: auto; }

/* The two small-image styles put the image beside the text once there is room. */
/* The two small-image styles put the image beside the text — but only where the cell is wide
   enough for two columns, which is the full-width band and the feature band's big cell. A media
   query alone cannot express that: the viewport says 72rem while a hero cell is a third of it, and
   the result is a 6rem image next to a heading broken over three lines. Everywhere else the image
   stays on top of the text, which is also what every cell does on a phone. */
@media (min-width: 40rem) {
    .band-full .cell-img-left,
    .band-full .cell-img-right,
    .band-feature > :first-child.cell-img-left,
    .band-feature > :first-child.cell-img-right {
        display: grid;
        grid-template-columns: minmax(8rem, 13rem) 1fr;
        column-gap: var(--sp-4);
        align-items: start;
    }
    .band-full .cell-img-left .cell-image,
    .band-feature > :first-child.cell-img-left .cell-image { grid-row: 1 / span 4; grid-column: 1; }
    .band-full .cell-img-right .cell-image,
    .band-feature > :first-child.cell-img-right .cell-image { grid-row: 1 / span 4; grid-column: 2; }
    .band-full .cell-img-right > :not(.cell-image),
    .band-feature > :first-child.cell-img-right > :not(.cell-image) { grid-column: 1; }

    /* Beside the text the image is narrower, but it is still the editor's picture: a 3/4 box turned
       every landscape upload into a centre crop with its wording cut off on both sides. The column
       width is the constraint, the ratio is the image's own. */
    .band-full .cell-img-left .cell-image img,
    .band-full .cell-img-right .cell-image img,
    .band-feature > :first-child.cell-img-left .cell-image img,
    .band-feature > :first-child.cell-img-right .cell-image img { max-height: 16rem; }
}

/* The wide styles say "let the image have the cell", not "recrop it to a banner" (the upload
   already is one) and not "blow it up to fill" either: `.cell-image` matches the bare <img> as well
   as the wrapping <a>, so the old `width: 100%` here drew a 188x55 upload 830px wide in a stacked
   aside tile. The cell is the ceiling; only the height cap is lifted. */
.cell-img-wide .cell-image { max-width: 100%; }
.cell-img-wide .cell-image img,
.cell-img-wide img.cell-image { max-height: none; }

/* THE FULL BAND IS FULL (asked for, 2026-08-18: the Studentlitteratur banner "is not aligned
   properly"). It is the one band with a single cell, so its cell is the whole column while the hero
   and second bands' cells are thirds of it — and the uploads are all the same 600-842px. In a third
   the picture is wider than the tile and scales down to fill it, so those rows are flush left and
   right; in the full band the picture is NARROWER than the tile, and `margin-inline: auto` then
   centred it, leaving a margin on both sides that no other row has. The lone banner floated in the
   middle of a block whose every other edge is a straight line.

   So this band, and only this band, treats the cell as a target rather than a ceiling — the same
   trade the slideshow took in the same pass: a modest upscale of one authored picture against an
   edge that lines up. The ceiling stays everywhere else (the aside tile's 188x55 upload is what that
   rule exists for — see above), and `height: auto` keeps the ratio, so this stretches nothing. */
/* The anchor has to be told too. `.cell-image` is `align-self: start`, which in the cell's COLUMN
   flex box is a cross-axis rule — the <a> shrinks to its content, so `width: 100%` on the <img>
   inside would have resolved against the picture's own width and changed nothing. */
.band-full .cell-img-wide .cell-image { align-self: stretch; }
.band-full .cell-img-wide .cell-image img,
.band-full .cell-img-wide img.cell-image { width: 100%; }

.cell-links { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--sp-1); }
.cell-links a { text-decoration: none; font-weight: 600; font-size: var(--fs-sm); }
.cell-links a:hover { text-decoration: underline; }
.cell-links .link-external::after { content: " ↗"; font-size: 0.8em; }

/* Tile accents — legacy's fixed Red/Blue alternation on the menu tiles and the
   two alternative puffs (anatomy §3.2). */
.ink-red { border-top: 4px solid var(--c-accent); }
.ink-blue { border-top: 4px solid var(--c-link); }

/* --- article puff --------------------------------------------------------- */

/* A PUFF IS THE SAME OBJECT AS AN AD-AREA CELL (asked for, 2026-08-18: "make these fit the theme").
   Both are white cards standing in a tinted region, and they were drawn with three differences that
   said nothing: the puff had a box-shadow where .start-cell has one only on hover, and a 4px ink rule
   along its top that no cell carries. In a 15rem column, two shadowed cards each topped by a coloured
   bar is the loudest thing on a page whose ad grid is deliberately flat.

   THE INK RULE IS ALSO A NO-OP, and that is the finding worth keeping: `.ink-red` is --c-accent and
   `.ink-blue` is --c-link, and in this palette both are #6a1a3e (they were both #00635e before the
   2026-09 reskin — the two tokens have simply never disagreed). Legacy's fixed Red/Blue alternation
   over the two puff slots (anatomy §3.2) therefore draws the SAME teal bar on both cards — the
   distinction it encodes cannot be seen and has not been since the tokens were set. Reset to the
   card's own border rather than deleted, so the classes stay on the markup (they are the legacy
   verdict) and come back on their own if the two tokens are ever given different values.

   The border stays. It is .start-cell's border, same token and same width, which is the whole point:
   a reader looking at the ad grid and the rail sees one kind of card in two places. */
.start-puff {
    display: grid;
    gap: var(--sp-3);
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 0;
    padding: var(--sp-4);
}

.start-puff.ink-red,
.start-puff.ink-blue { border-top: 1px solid var(--c-border); }

.puff-heading {
    margin: 0;
    font-size: var(--fs-sm);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--c-text-muted);
}

.puff-cover { justify-self: center; }
/* `contain`, for .card-cover img's reason: a fifth of the covers are square or wider (CDs, games,
   memo boxes) and a 2:3 crop takes a third of the picture off them. */
/* No shadow on the cover either, for the card's own reason above: the picture stands on the card's
   white and the card stands on the region's tint, and a shadow at each step is two lifts inside a
   block that is otherwise flat. The --c-badge-bg fill stays — it is what makes a missing cover read
   as an empty slot rather than as a hole in the card. */
.puff-cover img {
    display: block;
    width: 8rem;
    aspect-ratio: 2 / 3;
    object-fit: contain;
    background: var(--c-badge-bg);
    border-radius: 0;
}
.puff-body { display: grid; gap: var(--sp-2); }
.puff-title { margin: 0; font-size: var(--fs-base); }
.puff-title a { text-decoration: none; }
.puff-title a:hover { text-decoration: underline; }
.puff-author, .puff-description, .puff-more { margin: 0; font-size: var(--fs-sm); color: var(--c-text-muted); }
.puff-price { margin: 0; font-weight: 700; }

/* Replaces legacy's chopText: clamp by lines, and never lose the text itself.
   THREE LINES, NOT FOUR (2026-08-18). The blurb is the publisher's marketing copy and on these two
   slots it opens with pasted review quotes — "Sydsvenskan", "Dagens Nyheter" — so in a 15rem column
   four lines of it is a grey block that says nothing about the book and pushes the second puff's
   cover below the fold. Three is enough to be a lead-in. Nothing is lost that the reader cannot
   reach: the whole card links to the article, where the text is shown in full. */
.puff-description {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    overflow: hidden;
}

/* `.band-puffs` is gone (2026-08-18). It was the overflow band — the puffs that did not fit the
   one-row rail, rendered as cells of the ad grid — and the rail now spans the whole column beside the
   grid, so every puff is in it and nothing overflows. Removed here as well as in Start.cshtml: a
   class no markup emits is a rule that cannot be read as evidence of anything. */

/* --- banner strip ---------------------------------------------------------- */

/* Legacy hid all but one panel behind an hAccordion sized to fit 870 fixed pixels (§6.1, §6.3).
   Natively it is a slideshow, and it exists twice over:

     - Without JS the markup is what it says it is — a scroll-snap row that a finger, a trackpad or
       the keyboard pages. Nothing is hidden and no campaign is unreachable.
     - With JS (start.js) the row becomes a stack and one slide is shown at a time, crossfading on
       its own every few seconds, and dots that fill green over the hold — the clock and the manual
       control in one row of marks (no arrows, no separate bar, no pause button; see below).

   The fallback is not a courtesy: it is what makes the whole feature safe to build, because the
   campaigns never depend on a script to be seen. */
.start-strip { position: relative; display: grid; gap: var(--sp-3); }

.strip-list {
    display: flex;
    gap: var(--sp-3);
    margin: 0;
    padding: 0;
    list-style: none;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
}

.strip-list::-webkit-scrollbar { display: none; }

/* A SLIDE IS THE COLUMN WIDE (asked for, 2026-08-18: "make kampanj same size"). It was capped at
   46rem = 736px, the accordion upload's own width (746px) less a hair, so the picture was never drawn
   above its resolution. That cap was right while the column was the same 46rem; once the ad grid went
   full-width to meet the shelves it left the banner visibly narrower than every band under it — and a
   lead-in that is narrower than what it leads reads as a mistake, which costs more than the softness.

   The cost is real, bounded and measured: `.page` is --page-max 72rem less its 1rem padding each side
   = 1120px of content, and the column is that less the 15rem track and the sp-5 gap, so ~856px. A
   746px banner drawn at 856 is ~1.15x — a slight softening of one authored image, not a scale-up of
   the page. `object-fit: contain` and the untouched ratio mean nothing is cropped or distorted, only
   interpolated. If it ever reads as blurry the fix is a wider upload, not a narrower slide.

   The TRACK is what holds this number still: the rail was narrowed to 12rem on 2026-08-18 and the
   track deliberately was not, because the column is 1fr and every rem off the track is a rem of extra
   upscale here (see the 62rem block). */
.strip-panel {
    position: relative;
    flex: 0 0 100%;
    scroll-snap-align: start;
    background: var(--c-surface);
    border-radius: 0;
    overflow: hidden;
    /* Felt, not seen — the same ghost border the shelf cards take, for the same reason: a white
       slide on the near-white page ground is one step of the stair (docs/DesignSystem.md). */
    box-shadow: var(--ghost-border);
}

/* THE LABEL SITS ON THE PANEL'S FLOOR, so every slide's caption bottom is the same line (asked for,
   2026-08-18). A panel is picture-then-caption and both shapes stack them: the <a> holds the pair
   when the panel has an IMGLINK, the panel itself when it does not. Left as blocks, the caption sat
   wherever its own picture ended — and the pictures are editor uploads, so their heights differ
   whenever a ratio does (the slides also share one grid cell in the slideshow, where the tallest sets
   the height and the rest were simply short).

   `1fr auto` on whichever element is the parent, plus `align-self: end` on the picture: the caption
   row is its own content, the picture takes the rest and hangs from the caption. Nothing is scaled to
   make this true — a short picture sits lower in its own frame, which is the alignment being asked
   for, and no authored pixel is stretched to fake it. */
.strip-panel,
.strip-panel > a {
    display: grid;
    grid-template-rows: 1fr auto;
}
.strip-panel > a { height: 100%; text-decoration: none; color: inherit; }
.strip-panel > img,
.strip-panel > a > img { align-self: end; }

/* No crop: the accordion uploads are 746x256 banners with the campaign's own wording inside them,
   and a fixed ratio box cut the first and last word off every one. The height cap is there for a
   portrait upload; 26rem clears a 16:9 file at the slide's full width. */
.strip-panel img { display: block; width: 100%; height: auto; max-height: 26rem; object-fit: contain; }

/* THE CAPTION SITS UNDER THE PICTURE, not over it (asked for, 2026-08-17). It was a --fs-xl bold
   white line inset into the bottom of the banner over a black gradient, which is the standard hero
   treatment and the wrong one here for a reason particular to this control: **the accordion uploads
   are authored images with the campaign's own wording drawn inside them** (§6.1, and the whole reason
   the panel does not crop). A second headline laid over the first is two campaigns' worth of type in
   one frame, and the gradient needed to make it legible dims the artwork the editor paid for.

   Below the image it needs neither the shadow nor the scrim, so it can be small and quiet: --fs-sm in
   the label face, which is what the rest of the shop's UI furniture is set in.

   The highlight is the tonal accent pair (--c-accent-soft on --c-accent-soft-text, btj.css) — the
   shop's own "this is picked out" fill, the same one chips and selected states take, so it reads as
   part of the design system rather than as a colour chosen for this one strip. The left edge is the
   accent, the same 4px-rule idiom .shelf-heading uses — see "the campaign colour ordinals" at the
   end of the file for why it is not the campaign's own colour.

   One rule for both shapes now: a panel with no image used to need its own reset, because the caption
   was positioned over artwork that was not there. Nothing is positioned any more. */
.strip-label {
    display: block;
    padding: var(--sp-2) var(--sp-3);
    border-left: 4px solid var(--c-accent);
    background: var(--label-tint, var(--c-accent-soft));
    color: var(--label-ink, var(--c-accent-soft-text));
    font-family: var(--font-label);
    font-size: var(--fs-sm);
    font-weight: 600;
    line-height: 1.35;
}

/* THERE IS NO PER-SLIDE TINT ANY MORE (2026-09, the Designmanual reskin), and the block it
   replaced is worth recording because the request behind it was a real one.

   "A DIFFERENT TINT PER SLIDE" (asked for, 2026-08-17) rotated three tonal families by
   POSITION — 3n over the panels and, since 2026-08-18, over the dots in the same order so a
   mark and the caption it points at agreed. Position rather than the campaign ordinal
   (--c-tab, §2.3) because the ordinal is the editor's own colour and is not a palette:
   nothing stops an editor tagging every panel the same, and on tst and prd alike they
   routinely do — the live strip is three panels all `tab-btjgreen`.

   THE ROTATION NEEDED THREE PEER FAMILIES AND THE PALETTE HAS ONE. Its own comment named
   them: "teal (the primary), warm (the second voice), and the cool status tint", later
   indigo. After the reskin the first two are the same burgundy and the third hue is gone, so
   two of every three labels would have been identical — a rotation that rotates through one
   colour is not a distinction, it is a rule that looks like one.

   So the labels take .strip-label's own fallbacks (--c-accent-soft / -soft-text) and the dots
   take --strip-fill: one family for every slide, and the caption's 4px edge is the accent too.
   The honest cost is stated rather than glossed: the CAPTIONS do not distinguish slides at all
   and only aria-current and the fill animation do. If that is not
   acceptable the fix is a TONAL rotation (--c-accent-soft / --c-warm-soft / --c-surface-3, with the
   dot filling accent / accent-dark / --c-text) — weaker than three hues, but one hue is what
   the palette has. */

/* --- slideshow mode (start.js) --------------------------------------------- */

/* --- the crossfade (start.js) ----------------------------------------------- */

/* The change is a dissolve with a breath of movement in it, drawn by the slides themselves — no clone,
   no overlay, no callback (this replaced a 3D page turn that rotated a cloned sheet out of the frame).
   The whole of it is three properties on two grid items in the same cell, which is why the JS is down
   to "move an attribute".

   BOTH SLIDES FADE, and what makes that safe is the ground underneath. A symmetric crossfade is the
   obvious way to write this and was refused at first for a real reason: mid-dissolve both slides sit
   near half opacity, and whatever is BEHIND them shows through the pair — on the page's near-white
   ground that is a flash of the page between two white banners. The first version dodged it by not
   fading the outgoing slide at all (it held, then was cut once the incoming one was opaque over it),
   which dissolves correctly and gives the reader no sense of the old slide leaving. Painting the stack
   itself --c-surface removes the reason for the dodge: there is no page ground to show through, only
   the same white the panels already stand on, so both slides can fade and the seam stays invisible.

   The movement was a 2% scale for exactly this reason — a scale has no direction to get wrong, and the
   dots can be clicked in any order. It is a page turn as of 2026-08-18, and what changed is not the
   objection but the answer to it: show() computes direction as the shorter way round the ring instead
   of assuming the show only ever moves forward. The block below the stack rules is the turn; the
   dissolve described here survives in it only as the ground the sheets stand on, which is still what
   keeps the page from showing between them.

   `visibility`, not `display` or opacity alone: a hidden slide keeps its size, so the stack is as tall
   as its tallest slide with no height to compute in JS — and it leaves the tab order and the
   accessibility tree, which a transparent slide does not. Three live link sets stacked on top of each
   other is what opacity alone would cost. visibility does not interpolate, so it is transitioned at 0s
   with a delay: that delay is exactly what keeps the outgoing slide on screen long enough to fade. */
.start-strip {
    /* SMOOTHER, AND THE CURVE IS HALF OF IT (asked for, 2026-08-19: too aggressive). The roll
       shipped on cubic-bezier(0.22, 0.61, 0.25, 1) — away fast, settling long, which is the curve
       for something thrown. Read next to the picture it moves, that start is the aggression: the
       banner leaves at its highest speed, so the eye is chasing artwork from the first frame.
       Symmetric ease-in-out instead: it gathers, crosses, settles, and no part of the travel is
       the fastest by much. */
    --roll-ease: cubic-bezier(0.4, 0, 0.2, 1);
    /* AND THE OTHER HALF IS HOW FAR IT GOES. A full frame of travel means the whole 856px banner
       crosses the whole frame, which is a lot of movement to soften by timing alone — at any
       duration it is still a picture being thrown across a box. 40% is a drift: the slides overlap
       in the frame for most of the change and the CROSSFADE does the work of saying which one is
       arriving, with the movement there to give it a direction rather than to be the effect.
       A percentage, so it stays a proportion of the slide at every viewport. */
    --roll-dist: 40%;
    /* 600ms. The walk was 850 (page turn) -> 600 (roll) -> 800 -> 600 again, both of the last two
       asked for on 2026-08-19 after seeing them, and the pair is the record: 800 was tried on the
       argument that the whole banner crosses the frame here — 856px of artwork tracked by the eye,
       against a sheet that merely pivoted on a hinge — and seen next to each other, 600 is the one
       that reads as the show moving on rather than as a slide being walked across. start.js FADE_MS
       mirrors it: the two must agree, because that timer is what takes the leaving slide's mark
       off. */
    --fade-ms: 700ms;
    /* The dot fill (below). Brand teal — the shop's action colour, which is what the rest of the page
       draws a filled control in. It was green for a few turns (#2f8f3f, the --c-ok-* family one step
       brighter) and is teal after seeing both (asked for, 2026-08-17). The property is what makes that
       a one-line decision, and why the fill never names a colour itself. */
    --strip-fill: var(--c-accent);
}

/* THE SLIDESHOW IS THE WIDTH OF A SLIDE AND STARTS AT THE PAGE'S LEFT EDGE, aligned with
   .start-bands, and the dots sit right under the banner: the section's own gap is --sp-3, which is the
   spacing between two blocks that are separate things, and these are not — the marks belong to the
   picture above them (both asked for, 2026-08-17).

   There is no width cap here any more (2026-08-18, see .strip-panel): the strip takes the column, so
   the slide, the dots and the bands below all measure from the same two edges. The note the cap left
   behind is still worth keeping — a cap belongs to the SECTION, not to the slide inside it, because
   capping the slide centres it in the strip's width and puts two different left margins one above the
   other, while the section carries the dots along with it. */
.start-strip--show {
    gap: var(--sp-1);
}

/* The row becomes a stack: every slide in one grid cell, one of them current. Grid rather than absolute
   positioning so the stack still measures itself — the strip is as tall as its tallest slide, with no
   height to compute in JS and nothing to recompute on resize. The background is load-bearing, not
   decoration: it is the ground the dissolve happens over (see above). */
.start-strip--show .strip-list {
    display: grid;
    grid-template-areas: "slide";
    gap: 0;
    /* THE FRAME IS THE CLIP (2026-08-19, with the roll below). It was `visible` for the page turn,
       which needed a sheet to be seen lifting off the stack and casting past its own edges. A roll
       is the opposite: the slides travel through this box, and everything outside it is the part of
       the film that has not arrived yet — so a frame that does not clip shows two campaigns at once
       with a horizontal scrollbar under them. */
    overflow: hidden;
    scroll-snap-type: none;
    background: var(--c-surface);
}

/* IT ROLLS (asked for, 2026-08-19). The strip is a stack of slides in one grid cell and the change
   is a travel across the frame: the incoming campaign enters from the right, the outgoing one leaves
   to the left, both moving together at the same speed so the pair reads as one strip of film being
   wound on rather than as two pictures being swapped.

   THIS IS THE FOURTH ANIMATION HERE — travel, dissolve, page turn, roll — and the reason a roll can
   be this short a rule where the turn needed a whole section is worth keeping: **a roll has no
   depth**. The page turn had to say which sheet lay on which (z-index), light the paper as it came
   over (a ::after sheen and a keyframe set), cast a shadow onto the sheet underneath, and hold the
   leaving sheet opaque for two thirds of its travel so it did not dissolve while it turned. Every
   one of those exists to make two flat pictures read as two sheets at different depths. Slides that
   move sideways in one plane need none of it: the whole change is one transform per state.

   WHAT DOES NOT CHANGE, and is why this is a CSS-only edit: start.js sets `data-current` and
   `data-leaving` and nothing else — the same two attributes the turn used, and the dissolve before
   it. The script's own note already describes this model ("a waiting slide sits on the side it will
   enter FROM, a leaving slide travels to the other one"), which is the travel version's comment
   outliving two animations that had no use for it.

   ONE DIRECTION, ALWAYS, and that decision is start.js's rather than this file's: the show has no
   notion of going backwards for a reader looking at a sequence of campaigns, so clicking any dot
   means "take me to that campaign" and every one of those winds forward. Waiting slides therefore
   all sit to the RIGHT, and there is no from-state to compute and no direction attribute to carry.

   AND THE SLIDES FADE AS THEY TRAVEL (asked for, 2026-08-19: maybe something transparent). The
   roll shipped fully opaque, which is what film does — one frame replaces another and neither is
   ever half-there. Crossing them instead makes the change softer without making it a dissolve: the
   incoming banner is at nothing when it is a full frame out and reaches full strength as it lands,
   so most of the fade happens where the picture is off-centre and the eye is not reading it yet.
   Both slides are therefore partly transparent for the middle of the travel, and the frame's own
   --c-surface is what shows through, not the page.

   That last part is why the fade is safe here and would not have been under the page turn: the
   stack sits on the list's white ground (.start-strip--show .strip-list), so a half-opaque banner
   is composited against the same white the slide itself carries, rather than against whatever
   happens to be behind it. Nothing shows through to the ad grid below.

   VISIBILITY IS THE LOAD-BEARING WORD, and it is the one thing the turn's rules got for free. When
   start.js drops `data-leaving` (FADE_MS after the change) the slide falls back to the waiting rule,
   which means its transform goes from -40% to +40% — the whole way back across the frame. Under
   the turn that return was invisible because both states were `opacity: 0`. Here the waiting rule
   hides it in the SAME frame the attribute is removed (`visibility 0s linear`, no delay), so the
   return happens off-screen; the delay the turn's rules carried there would send the spent banner
   flying back across the picture in full view. */
.start-strip--show .strip-panel {
    grid-area: slide;
    flex: none;
    width: 100%;
    visibility: hidden;
    opacity: 0;
    transform: translateX(var(--roll-dist));
    transition:
        transform var(--fade-ms) var(--roll-ease),
        opacity var(--fade-ms) ease,
        visibility 0s linear;
}

/* The campaign being shown: in place and fully opaque. */
.start-strip--show .strip-panel[data-current] {
    visibility: visible;
    opacity: 1;
    transform: translateX(0);
    transition:
        transform var(--fade-ms) var(--roll-ease),
        opacity var(--fade-ms) ease,
        visibility 0s linear;
}

/* The one leaving, which needs a state of its own for the reason the script's note gives: "not
   current" is also every waiting slide's state, and those two want opposite sides of the frame.
   Held visible for the length of the travel; the rule above hides it after. */
.start-strip--show .strip-panel[data-leaving] {
    visibility: visible;
    opacity: 0;
    transform: translateX(calc(var(--roll-dist) * -1));
    transition:
        transform var(--fade-ms) var(--roll-ease),
        opacity var(--fade-ms) ease,
        visibility 0s linear var(--fade-ms);
}

/* NOTHING STAGGERS ANY MORE. The caption used to fade in a beat after the slide settled, which was
   right for a dissolve and for a turn: in both of those the picture arrives in place and the label
   can be read as a second event. A slide that travels carries its caption with it, so fading the
   label separately means the incoming banner arrives with a blank line under it — which reads as a
   slide that has not finished loading rather than as emphasis. */

/* --- slideshow controls ---------------------------------------------------- */

/* Dots below the frame rather than over the artwork: the banner is the editor's picture and every
   pixel of it is theirs. They are real buttons — a slideshow whose position is shown but not
   settable is a worse control than one with no dots at all.

   THE DOTS ARE THE CLOCK. The current one fills over the hold, and the slide changes when it is
   full: one mark says where the show is, how long is left, and takes the click that moves it. That
   is one affordance doing the work of three, and it replaces the separate progress bar this had —
   a bar and a row of dots are two answers to the same question, drawn a few pixels apart. */
.strip-dots {
    display: flex;
    justify-content: center;
    gap: var(--sp-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Equal widths, including the current one. An active dot that grows would move the whole row while
   it fills, and the fill has to be the only thing that changes for the eye to read it as a clock. */

/* THE EMPTY TRACK IS TONAL GREY, NOT WHITE, AND THE BAR IS 4.5rem x 0.55rem — reported invisible and
   fixed 2026-08-17. It was a 2.25rem x 0.4rem white pill carrying the ghost border (a 15%-opacity
   inset line, btj.css) on the page's near-white ground, and the reasoning behind that — "the fill has
   to read as paint arriving on a blank track" — held for the paint and forgot the track: at 0% there
   was a 36 x 6px near-white mark on near-white that could not be found at all, so the first thing the
   reader had to see was the thing least visible on the page.

   A progress bar has to be legible EMPTY. It is the control that says the page is about to change, so
   it has to exist before the fill starts and be findable while it is still short. Three changes, all
   the same point:

     - tonal grey track (--c-surface-4), the shop's own way of drawing a recess without a border
       (docs/DesignSystem.md) — it is what makes green-on-grey read as filled-vs-unfilled;
     - 0.35rem tall and 3rem wide.

   Those two numbers were 0.55 x 4.5rem for one turn — the bar was made big to fix the invisibility
   before the grey track was in — and were cut back once it could be seen (asked for, 2026-08-17).
   That order is the point: the TRACK is what made it findable, not the size, so the size was free to
   go back to something proportionate to a 46rem banner. 3rem still gives the fill 48px of travel over
   the 8s hold, twice what the original white bar had.

   --dot-h exists because the coarse-pointer block below computes padding from the mark's height in two
   places; a literal would have to be right in three. */
.strip-dot {
    --dot-h: 0.35rem;
    position: relative;
    width: 3rem;
    height: var(--dot-h);
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: var(--c-surface-4);
    overflow: hidden;
    cursor: pointer;
    transition: background-color 160ms ease;
}

.strip-dot:hover { background: var(--c-surface-3); }

/* EACH DOT FILLS IN ITS OWN SLIDE'S COLOUR (asked for, 2026-08-18), which is a reversal of the rule
   this file argued for two turns ago and worth keeping the argument for. The case against was that the
   ordinal answers "which campaign is this" while the bar answers "how long until the page changes", so
   colouring the clock per slide teaches the reader a hue that means nothing about time. The case for,
   which won: the row of marks sits under a slide whose caption already draws that ordinal as a 4px
   rule, so the dots are read as a legend as much as a clock — and a legend in a colour that matches
   nothing on the slide it points at is the worse of the two confusions. The measurement is still one
   thing; it is the *mark* that is per campaign, and the fill grows the same way in every one.

   The colour WAS the slide's position family (--label-strong, rotating teal → orange → indigo), not
   its campaign ordinal — because that is what the caption above it was tinted with, and matching the
   ordinal would have painted every dot the same on live data, where an editor tags all three panels
   alike. That rotation is deleted (2026-09: it needed three peer hues and the palette has one), so
   --label-strong is now set by nothing and every dot falls back to --strip-fill. The dots are one
   colour; what tells them apart is `aria-current` and the fill.

   A REAL ELEMENT, not the `::after` this used to be, and no `animation` either — start.js builds a
   `<span class="strip-dot-fill">` in every dot and writes its scale once per frame (see frame()).
   Script cannot set a style on a pseudo-element: the only handles CSS leaves for one are the selector
   it hangs off and a custom property, and both of those mean "declare an animation and hope it runs".
   Driving it from a rAF loop also merges the two clocks this had — a CSS animation over --hold-ms and
   a setTimeout of the same length, which could not be paused together — into the one accumulator that
   both paints the bar and decides when the slide changes. There is no --hold-ms in this file any more
   and no @keyframes: the hold is HOLD_MS in start.js and nowhere else.

   scaleX rather than width or `inset`: it is the one property every engine animates off the main
   thread, and a solid bar has nothing inside it that a scale could distort.

   Direction is the transform origin and nothing else — one word, no keyframes, no JS change, since the
   loop only ever writes a number between 0 and 1. `left` grows the paint rightwards from the left edge
   (reading direction, and where the eye already is); `right` pins it to the right edge and grows it
   leftwards. It shipped `right` on request and is `left` after seeing it (2026-08-17). */
.strip-dot-fill {
    position: absolute;
    inset: 0;
    border-radius: inherit;
    /* --label-strong was the per-slide family, set by an nth-child rotation deleted in 2026-09 (the
       reasoning is where that rotation used to be, above .strip-label). NOTHING SETS IT NOW, so this
       always resolves to --strip-fill; the var() is kept because it is the seam a tonal rotation
       would come back through, and removing it would make that a two-file change. */
    background: var(--label-strong, var(--strip-fill));
    transform: scaleX(0);
    transform-origin: left center;
}

/* Nothing here pauses the fill. Freezing it is not a CSS state any more — the loop that paints it
   simply stops spending time while the strip is held (start.js frame()), so the bar stands still
   because nobody moved it, which is one mechanism instead of `animation-play-state` plus a timer that
   had to be stopped separately.

   What the class is still for is saying so. The pause needs a mark of its own, because a frozen fill
   is only legible once there is some fill to freeze. The strip leads the page and is a full-width banner, so a cursor merely resting there holds
   the show — and held at 0% the current dot was identical to an empty one, which reads as broken
   rather than as paused. The track takes the fill's own colour at low strength: the dot the show is on
   is now findable whether it is filling, frozen or frozen at zero. */
.start-strip.is-paused .strip-dot[aria-current="true"] {
    background: color-mix(in srgb, var(--label-strong, var(--strip-fill)) 22%, var(--c-surface-4));
}

/* The finger target is the row, not the mark: the mark stays --dot-h tall however tall its button is,
   so the padding grows the hit area while a content-box clip keeps the mark its own size. The fill
   needs the same treatment — it is inset to the content box here — and it is still the one scaleX
   written by start.js: the geometry is a static rule per pointer type and the movement is the same
   everywhere. */
@media (pointer: coarse) {
    .strip-dots { gap: var(--sp-1); }
    .strip-dot {
        height: var(--control-min);
        background-clip: content-box;
        padding: calc((var(--control-min) - var(--dot-h)) / 2) 0;
    }
    .strip-dot-fill { inset: calc((var(--control-min) - var(--dot-h)) / 2) 0; }
}

/* The shelves' paging arrows. The strip had a pair too and does not any more: its dots sit under
   the frame, they move the show as well as report where it is, and two overlapping affordances over
   one banner was one too many. The shelves keep theirs because a scrolling row has no dots and no
   fixed number of positions to give any.

   A tonal control, not a bordered-and-shadowed one: a soft fill that is the shop's own way of saying
   "a control you may press", carrying its own edge, so the line and the lift both go — the same trade
   the shelf card and the DH cover make. 2.75rem stays; that is the finger target.

   SMULTRON AT REST (2026-09-11, drawn from the designer's screenshots): the shop's "selected" fill,
   with the burgundy glyph on it. It sits ON the artwork — `top: 45%` puts it over the cover, not
   beside it — so what holds it against a picture nobody here chose is the round light shape and the
   dark glyph, not the tint. And **the shelf's two controls are one control**: the arrow and the
   scrollbar under it do the same job, moving the same row, so they are drawn in the same family —
   burgundy on hover here, burgundy for the scrollbar's thumb.

   Hover is --c-warm-dark, the same hover the scrollbar thumb takes: white on it is 14.4:1, which
   matters because this glyph sits over photographs. */
.slide-nav {
    /* --nav-size is the one number: the box and the offset that centres it are both read off it, so the
       control cannot drift out of step with its own centring (that offset used to be a literal 1.4rem
       against a 2.75rem box). 2.75rem is --control-min, the finger target. */
    --nav-size: 2.75rem;
    position: absolute;
    top: calc(50% - var(--nav-size) / 2);
    z-index: 2;
    display: grid;
    place-items: center;
    width: var(--nav-size);
    height: var(--nav-size);
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--c-accent-soft);
    color: var(--c-accent-soft-text);
    font-size: 1.25rem;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    transition: opacity 120ms ease, background-color 120ms ease, color 120ms ease;
}

/* Hover is the strong end of the same pair, which is where the soft fill points anyway. */
.slide-nav:hover { background: var(--c-warm-dark); color: #fff; }
.slide-prev { left: var(--sp-2); }
.slide-next { right: var(--sp-2); }
.slide-nav[disabled] { opacity: 0; pointer-events: none; }

/* On the shelves the arrows belong over the covers, not over the price lines. */
.start-shelf .slide-nav { top: 45%; }

/* --- carousel shelves ----------------------------------------------------- */

/* THE SHELF STANDS ON THE PAGE'S OWN GROUND (2026-09-11, drawn from the designer's screenshots): a
   shelf is its heading and a row of white cards on --c-bg, and what separates one shelf from the
   next is the heading's 4px accent rule and the space between them, not a tinted band. The white
   cards on the near-white --c-bg are a single step of the stair, which is why they carry the ghost
   border. No inline padding, for .start-bands' reason: with no ground it is an invisible frame, so
   the heading's rule and the first card start on the page gutter. The block padding stays as the
   space between shelves. */
.start-shelf {
    position: relative;
    display: grid;
    gap: var(--sp-3);
    padding: var(--sp-4) 0 var(--sp-3);
}

.shelf-heading {
    margin: 0;
    font-size: var(--fs-xl);
    border-left: 4px solid var(--c-accent);
    padding-left: var(--sp-3);
}

.shelf-list {
    display: flex;
    gap: var(--sp-4);
    margin: 0;
    /* The bottom padding is the gap between the cards and the scrollbar under them, so it grows with
       the bar (--sp-4 since the bar went from `thin` to `auto`). */
    padding: 0 0 var(--sp-4);
    list-style: none;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    overscroll-behavior-x: contain;
}

/* THE SHELF'S SCROLLBAR IS PART OF THE DESIGN, not the operating system's (asked for, 2026-08-17).
   It was `thin` with a --c-border thumb: a grey hairline of Windows furniture under a row of the shop's
   own cards, and on this page it is not furniture at all — it is the shelf's only always-visible
   affordance. The arrows are a mouse-only extra that vanish at the ends and are hidden outright on
   touch, so the bar is what says "this row goes on".

   A BURGUNDY THUMB ON NO TRACK (2026-09-11, drawn from the designer's screenshots): --c-warm thumb,
   --c-warm-dark on hover, and a transparent track, so the bar is one pill of the accent on the page's
   own ground. The thumb is the affordance; a tinted track under it was one more band on a page that
   no longer draws bands.

   BOTH SYNTAXES, UNCONDITIONALLY, AND THE COLOUR IS WHAT SURVIVES. The standard properties and the
   ::-webkit-scrollbar pseudo-elements do not compose: in Chromium a non-`auto` scrollbar-color or
   scrollbar-width makes it ignore every pseudo, and Firefox has no pseudos at all. That was first
   written as an @supports gate — the standard pair scoped to `(not selector(::-webkit-scrollbar))`, so
   that each engine got the syntax it prefers and Chromium kept the pill radius. It shipped grey in the
   browser it was tested in, because the gate rests on a guess about how an engine answers `selector()`
   for a pseudo-element it does not implement but may still parse, and a wrong guess there closes the
   only door that engine had.

   So: no gate. Every engine with the standard properties uses them and draws a burgundy bar in its own
   shape; the pseudos below stay for engines that have those and not these, where they also add the pill
   radius. What is lost is the radius in Chromium. What is bought is that the bar is burgundy in every
   engine, which is the whole request — **a decoration worth a feature query is not worth a feature
   query when the fallback of getting it wrong is the plain grey you were replacing.** */
/* BIGGER, 2026-08-18. `auto` rather than `thin`: the standard property has only those two widths and
   no length, and in the engines that honour it (which is every current one — see the note above about
   why the pseudo-elements are not what Chromium is drawing here) `thin` is roughly half the platform
   bar. The bar is this shelf's only always-visible affordance, so half a bar is the wrong half.
   The ::-webkit height moves with it for engines that draw the pseudos and not the standard pair. */
.shelf-list {
    scrollbar-width: auto;
    scrollbar-color: var(--c-warm) transparent;
}

.shelf-list::-webkit-scrollbar { height: 0.85rem; }

.shelf-list::-webkit-scrollbar-track {
    background: transparent;
    border-radius: 999px;
}

.shelf-list::-webkit-scrollbar-thumb {
    background: var(--c-warm);
    border-radius: 999px;
}

.shelf-list::-webkit-scrollbar-thumb:hover { background: var(--c-warm-dark); }

/* The card is as wide as its cover can be drawn, not as wide as the shelf allows. The largest
   rendition the image pipeline stores is 118-139px across (docs/SearchAnatomy.md §4.1), so an 11rem
   card asked the browser to enlarge every cover by about 1.4x -- measured 176x264 against a 118x180
   file. 8/8.5rem lands just above the file's own width: a card any narrower starts squeezing the
   title, and one wider only magnifies. Revisit this if a bigger rendition ever arrives. */
/* A column rather than a grid, so the price can be sent to the bottom: titles run one to three
   lines and a price that simply follows the text lands at a different height on every card, which
   reads as a ragged row of numbers. The cards are already equal height (the flex row stretches
   them), so bottom-aligning the price puts every one on the same line across the shelf. */
/* The card is a card: its own ground, a radius and padding, the way a DH article card is
   (haftet.css .dh-card) and the article detail's cover already is. Before this the shelf was
   loose text on the page ground with a shadowed cover floating over it — the one surface left in
   the shop still lifting a cover instead of standing it on something. Tonal layering, no line
   (docs/DesignSystem.md).
   The flex-basis grew by exactly the padding it gained, so the COVER is still the 8/8.5rem the
   comment above sizes it to — the card got wider, the picture did not change. */
.shelf-card {
    flex: 0 0 9.5rem;
    scroll-snap-align: start;
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
    padding: var(--sp-3);
    background: var(--c-surface);
    border-radius: 0;
    /* The one line on the page, and it is the one DH already draws: white on --c-bg is a single
       step of the stair and reads as barely a boundary, so a forward card takes the ghost border —
       felt, not seen (haftet.css .dh-menu-card, docs/DesignSystem.md). It is a box-shadow, so it
       costs no layout, and dark mode nulls the token. */
    box-shadow: var(--ghost-border);
    transition: background-color 120ms ease;
}

/* Hover is one tone deeper, not a lift — the same reason the shadow went. */
.shelf-card:hover { background: var(--c-surface-2); }

/* The 2:3 box is the shelf's rhythm — every card the same height, titles on one line across the
   row — but the picture inside it is not always book-shaped: of eighteen real covers on tst, four
   are square or wider (CDs, games, memo boxes) and `cover` was cutting about a third off them.
   `contain` keeps the box and letterboxes the odd shapes against the tile fill below. A book fills
   the frame either way, so most of the shelf is unchanged. */
/* NO FILL BEHIND THE COVER (asked for, 2026-08-17). The 2:3 box stays — it is the shelf's rhythm, and
   dropping it would let every odd-shaped cover set its own height and leave the titles and prices
   ragged across the row — but the leftover area around a square or wide picture is no longer painted
   --c-surface-2. That tint was drawing a grey frame around a fifth of the shelf: on the covers that DO
   fill the box it was invisible, so it read as a defect on the ones that do not rather than as a
   deliberate tile, and the card is already standing on its own white ground.

   Transparent, not white: the letterbox now shows the card's surface, whatever that is — including the
   one tone deeper the card takes on hover, which a hard-coded white would have sat on top of. */
.card-cover img {
    display: block;
    width: 100%;
    aspect-ratio: 2 / 3;
    object-fit: contain;
    border-radius: 0;
    background: transparent;
}

.card-title { margin: var(--sp-2) 0 0; font-size: var(--fs-sm); font-weight: 600; }
.card-title a { text-decoration: none; }
.card-title a:hover { text-decoration: underline; }
.card-author, .card-media { margin: 0; font-size: var(--fs-sm); color: var(--c-text-muted); }
/* margin-top: auto is the alignment: it eats the card's leftover height, so the price sits on the
   card's bottom edge whatever the title did above it. A card without a price simply ends early. */
.card-price {
    margin: var(--sp-1) 0 0;
    margin-top: auto;
    padding-top: var(--sp-1);
    font-size: var(--fs-sm);
    font-weight: 700;
}

/* THE CAMPAIGN COLOUR ORDINALS ARE NOT DRAWN (2026-09-11, drawn from the designer's screenshots).
   The editor's CMP colour (anatomy §2.3, §6.2) still reaches the markup as a .tab-* class on every
   strip panel and shelf (StartDisplay.ColorClass), and no rule here reads it: the caption's edge and
   the shelf heading's rule are the accent, so the landing is burgundy and Smultron like the rest of
   the shop. The class is left on the markup so that drawing the ordinal again is a stylesheet
   change — one custom property per class and a var() at the two 4px edges. */

/* --- wider screens -------------------------------------------------------- */

/* --- phones --------------------------------------------------------------- */

@media (max-width: 47.99rem) {
    /* Let the two scrollers reach the screen edges instead of stopping inside the page gutter —
       a row that ends 1rem from the edge reads as clipped, not as scrollable. The negative margin
       is exactly .page's padding, and scroll-padding puts the snapped slide back inside it. */
    .strip-list,
    .shelf-list {
        margin-inline: calc(var(--sp-4) * -1);
        padding-inline: var(--sp-4);
        scroll-padding-inline: var(--sp-4);
    }

    /* Arrows are a mouse affordance; a touch screen has the swipe the snapping was built for. */
    .slide-nav { display: none; }

    /* No phone override any more: the caption is --fs-sm everywhere since it stopped being a
       headline laid over the banner, and the old rule made it BIGGER on a phone than on a desktop. */
    /* No card override here any more: the phone width used to be the narrow exception to a 10rem
       card, and the base is now narrower than it was, so repeating 8.5rem here would make a phone's
       cards WIDER than the rule it was written to undercut. */
    /* THE ONE REGION WITH A GROUND GOES EDGE TO EDGE: a burgundy block that stops 1rem short on
       both sides reads as a card of its own, so the start page's shortcuts rail bleeds by the page
       gutter and its own padding-inline puts the tiles back on it. The ad grid, the puff rail and the
       shelves have no ground and no inline padding, so they simply sit on the gutter — and the
       .shelf-list bleed above is measured from it, so a shelf's row still reaches the glass. */
    .start-aside:not(:has(> .start-puff)) { margin-inline: calc(var(--sp-4) * -1); }

    .shelf-heading { font-size: var(--fs-lg); }
    /* `.start-title { margin-bottom: -sp-2 }` was here, pulling the content up under the h1. The
       heading has been .start-title-hidden — `position: absolute`, out of flow — since 2026-08-18,
       so the rule has had nothing to pull against since; a negative margin on an absolutely
       positioned box moves only that box, and this one is 1px square behind a clip-path. */
}

/* The aside is a sidebar, not a second main column: its tiles are compact and their headings sit
   one step down from the grid's. */
.start-aside .start-cell { padding: var(--sp-2); }
.start-aside .cell-heading { font-size: var(--fs-base); }
.start-aside .cell-text { font-size: var(--fs-sm); }

@media (min-width: 48rem) {
    .start-page { gap: var(--sp-8); }
    .band-feature { grid-template-columns: 3fr 1fr; }

    /* …UNLESS THE BAND IS TWO BARE BANNERS (asked for, 2026-08-18: "why is booktok so small?").
       Measured: the Booktok upload is 600x383 and was drawn ~202px wide, a third of its own
       resolution, while the picture beside it (495x150) drew at its full width — one normal picture
       and one tiny one in a row that is supposed to be a pair.

       The 3:1 is not arbitrary and is right where it was written: slot 4 is the feature cell, which
       renders its image BESIDE its text (.band-feature > :first-child.cell-img-left, the 40rem block
       above), and that inner two-column layout needs the width. It buys room for text. A band whose
       cells are all .cell-img-wide has no text in it at all, so the 3fr is paying for something that
       is not there and the 1fr cell pays the bill — and `.cell-image img` is `width: auto`, a ceiling
       and never a target, so the cell's width IS the picture's.

       `:not(:has(> .start-cell:not(.cell-img-wide)))` — "no cell here is anything but a bare banner".
       Stated as the double negative rather than :has(.cell-img-wide) because one banner beside a
       text cell is still the case the 3:1 exists for; it is every cell or none.

       SCOPED TO THE MEDIA LANDING, and that is a deliberate narrowing rather than the general rule
       (asked for, 2026-08-18). The argument above holds on / just as well, but slot 4 is an Allt slot
       (StartPageAnatomy.md §333) — so a general rule would change / on a day the editor fills slots 4
       and 5 with bare banners, and / is to stay as it is. Since 2026-09-14 / has a puff rail too
       (StartDisplay.RailPicks), so the rail no longer tells the two apart and :not(.media-allt) does
       — the narrowing is the only thing keeping the two landings from drawing an identical band
       identically. */
    .start-page:not(.media-allt) > .start-columns:has(> .start-aside > .start-puff)
        .band-feature:not(:has(> .start-cell:not(.cell-img-wide))) {
        grid-template-columns: 1fr 1fr;
    }
    /* 8.5rem of cover = 136px, the widest the pipeline actually stores, plus the card's padding.
       See .shelf-card. */
    .shelf-card { flex-basis: 10rem; }
}

/* HISTORY of this breakpoint's tracks, because both earlier shapes are wrong for a reason worth
   keeping. `1fr 20rem` (before 2026-08-17) let the column take every pixel and the slide, capped at
   46rem for the upload's own resolution, ended well short of the rail — dead space between two things
   on the same row. `minmax(0, 46rem) 20rem` (2026-08-17) fixed that by capping the whole COLUMN at the
   slide's width, which lined the banner up with the rail and then misaligned the ad grid with the
   shelves below instead: the cap belonged on the slide, not the column. The current shape puts it back
   there — see the block below. */
/* THE RAIL IS ON THE LEFT (asked for, 2026-08-18) — placed, not reordered. The markup keeps the main
   column first, because that is the order the single-column layout below this breakpoint has to stack
   in (the shortcuts are a shortcut *from* the campaigns; a rail that precedes them in source would be
   read first by a screen reader and scrolled past by everyone on a phone). Only the two-column grid
   swaps the sides, with explicit tracks. */
@media (min-width: 62rem) {
    /* 15rem, not 20 (asked for, 2026-08-18: "make this content smaller"). The rail's tiles are
       authored banners whose width is whatever the tile gives them, so the track IS the tile size —
       shrinking the picture is shrinking the column. 15rem leaves ~13rem of picture inside the
       padding, well under the 750-842px the uploads actually carry, so this only ever scales them
       down and never past their own resolution.

       THE MAIN COLUMN IS 1fr AGAIN, NOT 46rem (asked for, 2026-08-18: bigger, and flush with the
       shelves). The shelves are siblings of .start-columns and span the whole page, so a capped
       column left the ad grid's right edge short of every shelf below it — two blocks of the same
       page ending at two different lines. 1fr puts that edge on the page's, which is the shelf's.

       The slideshow was capped at 46rem for one turn after this and that was refused too (asked for,
       2026-08-18: "make kampanj same size") — a lead-in narrower than the bands it leads reads as a
       mistake. The cap is gone from .strip-panel, with the resolution cost recorded there. The ad
       grid's own cells never enlarge anything: they are `width: auto` images centred in the tile, so
       a wider column gives them room and nothing more. */
    /* THE TRACK STAYS 15rem AND THE RAIL SHRINKS INSIDE IT (asked for, 2026-08-18: make the rail
       smaller, and *do not change kampanj*). Those two are in tension, because the main column is
       1fr: every rem taken off the track is a rem handed to the strip and the bands, so narrowing
       the track is a way of enlarging the slideshow by the back door. The track is therefore the
       page's layout — untouched, so nothing in column 2 moves a pixel — and the aside is narrower
       than the track it sits in.

       10rem of rail (20 -> 15 -> 12 -> 10 across 2026-08-18, each step asked for): the tiles are
       authored banners whose width is whatever the tile gives them, so the rail's width IS the
       picture size. Measured uploads are three 842x314 banners and one 188x55 logo tile, so at 10rem
       the banners draw ~144px wide, a fifth of their own resolution and the sharpest they have ever
       been drawn here. The 188x55 logo tile is no longer drawn at all — an unlinked, alt-less
       picture in a cell that carries links, which the view now declines to render
       (AdAreaCellView.ImageIsDecoration) — so nothing in the rail refuses to shrink with it.

       10rem was close to the floor, and the floor is TEXT, not picture: the first tile is the four
       Genvägar links, and "BTJ-häftet mm." at --fs-sm needs roughly 100px of line. Narrower than that
       and the links wrap, which makes the rail taller — the opposite of what a narrowing is for.

       AS OF THE SAME DAY THE RAIL FILLS THE TRACK AGAIN (see the .start-aside rule below): the width
       above is history, and what it leaves behind is the reason the TRACK is the number that matters.
       The rail may be any width up to it without a pixel moving in column 2; the track cannot change
       without resizing the slideshow. */
    /* THE TRACK IS NAMED ONCE, because three rules need the same number and one of them is not in
       this grid at all: the tracks here, the mirrored media landing's swapped tracks below, and the
       shelf width that has to match the ad grid — and a shelf is a SIBLING of .start-columns, so it
       can only be given the column's width by arithmetic. A literal repeated in three places is a
       shape that can be half-retuned, and the note above is emphatic that the track is the one
       number that must not move by accident. Declared on .start-page so every one of the three can
       see it. */
    .start-page { --rail-track: 15rem; }

    .start-columns { grid-template-columns: var(--rail-track) minmax(0, 1fr); align-items: start; }

    /* ROW 1 IS THE STRIP AND THE RAIL; ROW 2 IS THE AD GRID, ACROSS BOTH (asked for, 2026-08-18: the
       bands have to be as wide as the shelves). All three are placed explicitly — DOM order is
       strip → bands → aside, the single-column stack below this breakpoint, and none of the three
       would land where it belongs by auto-placement.

       The track is untouched at 15rem, so the strip is the same ~856px it was: this moves the BANDS
       (856 -> the full 1120px of .page content, which is exactly the shelf sections' width) and
       nothing else. An earlier attempt at this shape narrowed the track to 26rem for a 2x2 rail and
       was refused for taking ~180px off the slideshow to pay for it.

       Row 1 has no hole in it any more, which is why this works now and did not before: the rail is
       ~330px against the strip's ~355px, once the rail went to 10rem and its logo tile stopped
       rendering its picture. A taller aside would simply set the row height and leave blank ground
       under the strip — worth re-measuring if the rail ever grows back. (Since the subgrid above,
       row 1 is the slide list alone and the dots are row 2, so those two numbers are compared without
       the dot row in one of them.) */
    /* THE STRIP IS TWO ROWS, AND THE RAIL IS ONLY THE FIRST ONE (asked for, 2026-08-18: the rail's
       height has to be the PICTURE's, and kampanj must not change size). The strip section is the
       slide list plus the dots under it, and while it was one row the rail — stretched to that row —
       came out a dot-row plus a gap taller than the banner beside it. Every earlier attempt at this
       measured the wrong thing, because .start-strip is what the columns grid can see and .strip-list
       is what the eye compares.

       `grid-template-rows: subgrid` is what lets the OUTER grid see the inner one: the strip keeps its
       own two children in its own order and its own --sp-3 gap, but its rows ARE rows 1 and 2 of
       .start-columns. So the rail can be placed on row 1 alone — the slide list's row, to the pixel —
       and the dots sit in row 2 beside nothing, which is where they already were visually.

       Nothing in column 2 moves: the tracks are untouched, the strip still spans exactly the height it
       did, and the two rows it now occupies are the same two boxes its own grid drew before. The
       arrows are `position: absolute` (.slide-nav) and take no track, so they are unaffected either
       way. The bands move down one row number and nowhere on the page.

       align-self: stretch because .start-columns is `align-items: start`, and a subgrid whose tracks
       must line up with its parent's cannot be start-aligned inside them. */
    /* NO RAIL, NO SECOND COLUMN (asked for, 2026-08-18). With the Genvägar rail unrendered
       (Start.cshtml) the Allt landing has nothing in column 1, and a 15rem track holding nothing is
       15rem of the slideshow given away — the strip would still start where the rail used to end.
       Keyed on the rail's absence rather than on a page flag, so the media landings, which still
       emit a puff rail, keep the two-column row untouched.

       The strip has to be re-placed as well as re-tracked: its `grid-column: 2` below would open an
       implicit second column and put the slide back where it was. */
    .start-columns:not(:has(> .start-aside)) {
        grid-template-columns: minmax(0, 1fr);
    }

    .start-columns:not(:has(> .start-aside)) > .start-strip {
        grid-column: 1;
    }

    .start-columns > .start-strip {
        grid-column: 2;
        grid-row: 1 / span 2;
        grid-template-rows: subgrid;
        align-self: stretch;
        /* AND ITS ROW GAP HAS TO BE THE OUTER GRID'S, TO THE PIXEL. This is the whole of why the
           first version of this rule changed nothing anyone could see, and it is worth stating as a
           rule because nothing about subgrid warns you: **a subgrid has one set of gutters, and they
           are the parent's** — the strip's own `gap` is what it is MEASURED with and the parent's is
           what its rows are LAID OUT with, so every pixel of difference between the two comes off the
           tracks.

           Measured, with the strip at --sp-1 (4px) inside a grid at --sp-5 (24px): the strip sized
           itself 357.42 tall (slide 328.63 + 4 + dots 24.80), the outer grid then laid two rows plus
           a 24px gutter inside that fixed 357.42, and the 20px shortfall came off both rows equally —
           row 1 measured 318.63 against a 328.63 slide. So the rail came out 10px short of the
           picture and the slide overflowed its own row by 10: visible on a ruler, invisible on the
           page, and exactly the "no difference" that was reported.

           The equality is arranged the other way round — the outer grid takes the strip's --sp-1
           rather than the strip taking the outer grid's --sp-5 — because the strip's number is the
           one that is SEEN: it is the space between the slide and its dots, the control's own
           spacing, and the thing that must not change. The page's --sp-5 between the campaign block
           and the ad grid is then put back as a margin on .start-bands, where it is a single edge
           rather than a gutter shared with anything.

           Stated here as well as on .start-columns because this is the half that would be edited: the
           strip's gap is --sp-3 in the fallback and --sp-1 once start.js turns the slideshow on, and
           pinning it here is what keeps the two modes from having to be matched separately. With no
           dots there is no second child and no gutter to see, so the fallback is unchanged by it. */
        row-gap: var(--sp-1);
    }
    /* THE OUTER ROW GUTTER IS THE STRIP'S — see the note above; the two have to be one number, and
       this is the half that is not seen. */
    .start-columns {
        row-gap: var(--sp-1);
    }
    /* Row 3, and the page's own --sp-5 above it as a margin rather than as the grid's gutter.
       Expressed as the subtraction it is, so it follows the tokens if either is retuned. */
    .start-columns > .start-bands {
        grid-column: 1 / -1;
        grid-row: 3;
        margin-top: calc(var(--sp-5) - var(--sp-1));
    }
    /* AND IT IS NUDGED RIGHT BY THE DIFFERENCE IN THE TWO PADDINGS (asked for, 2026-08-18: align it
       with Noveller). Both blocks start at the page's left edge, so their GROUNDS already agree —
       what the eye follows is the content inside them, and .start-shelf insets its heading and cards
       by --sp-4 where the rail insets its tiles by --sp-2. The rail's tiles therefore stood 8px to
       the left of every shelf below them.

       A margin rather than a fatter left padding: the rail is a fixed 10rem, so padding would have
       taken the 8px out of the pictures instead of out of the slack, and there is 5rem of slack in
       the 15rem track doing nothing. Expressed as the subtraction it is, so it follows the tokens if
       either padding is ever retuned. */
    /* THE RAIL FILLS ITS TRACK (asked for, 2026-08-18: wider, it sat too far from kampanj). At 10rem
       with `justify-self: start` there were ~5rem of slack between the rail's right edge and the
       strip — the track was the page's layout and the rail was a narrow thing parked at the left of
       it, so the gap the eye saw was the slack plus the grid gap, and the two blocks read as
       unrelated rather than as two columns of one row.

       Stretching to the track is the version that closes it WITHOUT moving anything in column 2: the
       track is still 15rem and the main column is still 1fr, so the strip and the bands are the same
       width to the pixel. The margin is the tile alignment — the rail's own padding is --sp-2 and the
       page gutter is --sp-4, so the difference puts the first tile's edge on the gutter — and `width:
       auto` under `stretch` spends what is left of the track after it. */
    .start-columns > .start-aside {
        grid-column: 1;
        grid-row: 1;
        justify-self: start;
        /* AND THE BOX ENDS WHERE THE TILES DO (asked for, 2026-08-18: the right side has to match the
           left). Stretching to the 15rem track left the cards' right edge 41px inside the ground
           where their left edge is 16 — the tint's inset was --sp-4 on one side and --sp-4 plus
           whatever the height budget did not spend on the other, which reads as a block that is
           slightly crooked rather than as a margin.

           Sized off --rail-card instead, so the two insets are the same token by construction and
           stay equal when the card width is retuned. The TRACK is untouched at 15rem — this narrows
           the rail inside it, so nothing in column 2 moves and kampanj is the width it was. What it
           spends is the gap: the bare page between the rail and the slide grows by the 25px the
           ground gives up. */
        width: calc(var(--rail-card) + var(--sp-4) * 2);
        /* AND AS TALL AS THE SLIDE LIST, NOT TALLER (asked for, 2026-08-18: match the kampanj
           picture). Two halves, and both are needed. `align-self: stretch` overrides the container's
           `align-items: start` for this one child, so the rail's box takes the row's full height —
           which is the .strip-list's, since the strip subgrids its dots into row 2 and row 1 holds
           the slide list and the rail alone. That alone only ever makes the rail longer,
           so the second half is the flex block below: the rail's own content has to be able to fit
           inside the strip's height, or the rail sets the row and the strip is the one left with blank
           ground under it. */
        align-self: stretch;
        /* AND THE ONE WORD THE PREVIOUS TWO ATTEMPTS WERE MISSING. A grid item's automatic minimum
           size is `auto`, which resolves to its CONTENT's minimum — so however freely the tiles inside
           the rail were told to shrink, the rail itself still reported "I need all of this" when the
           row was measured, and the row grew to fit it. The stretch then had nothing to stretch into:
           the rail was already the tallest thing in the row.
           min-height: 0 removes that floor, so the row is measured from the strip alone and the rail
           is handed the result. Everything below only decides how the tiles divide it. */
        min-height: 0;
        /* NO MARGIN ANY MORE. It existed to push the rail's tiles onto the page gutter while the rail
           itself was inset by --sp-2 — the tiles were the only edge that mattered, because the rail
           had a tint and the tint's edge was read as the region's. With the ground gone the rail's
           box edge is what the eye lines up against the indigo block beside it, and the tile
           alignment moved into padding-inline above. Keeping both would put the box 8px inside the
           ad grid's. */
    }

    /* A TITLES RAIL IS ON THE RIGHT (asked for, 2026-08-18) — the same two-column row as a rail-less
       /, mirrored, and mirrored by PLACEMENT rather than by a second source order.

       Keyed on what is IN the aside (.start-aside:has(> .start-puff), the ground rule near the top of
       this file), never on a page flag: a rail holding puffs is this layout wherever it appears. That
       was a media-landing-only fact until 2026-09-14, when / gained a rail of titles picked from its
       own carousel shelves (StartDisplay.RailPicks) — and it is exactly what "put them top right, as on
       /bocker/" asked for, so these rules reach / now by design. A day with no shelves on / is a
       rail-less / again, and the single-column rule above takes over.

       Why the TRACKS are swapped and not just the aside re-placed: the rail's track is the 15rem one
       and the main column is the 1fr, so which side each block sits on is the track ORDER. Moving the
       aside into column 2 on its own would have handed it the 1fr and squeezed the campaign strip into
       15rem — the one outcome every note in this block exists to prevent.

       justify-self: end, not start. The rail is narrower than its track (width is --rail-card plus the
       page gutter, see the rule above), so on the left it parks against the page's left edge and on
       the right it has to park against the page's right one. Left there, the ~25px of slack would sit
       between the rail and the page edge and read as a margin that the ad grid and the shelves below
       do not have — two blocks of one page ending on two different lines, which is the same fault the
       1fr main column was introduced to fix. */
    .start-columns:has(> .start-aside > .start-puff) {
        grid-template-columns: minmax(0, 1fr) var(--rail-track);
    }
    .start-columns:has(> .start-aside > .start-puff) > .start-strip { grid-column: 1; }

    /* AND THE AD GRID IS THE STRIP'S WIDTH, NOT THE PAGE'S (asked for, 2026-08-18: "start-bands needs
       to be the same width as kampanj"). This is the one place the mirrored landing genuinely differs
       from / rather than reflecting it, and the reason is that the two pages have different things
       under the fold. On / the bands span both columns because the SHELF sections beneath them span
       the page, and two blocks of one page ending on two different lines is the fault that shape was
       introduced to fix (see the note on .start-columns > .start-bands). A media landing whose
       carousel is empty has no shelves at all — /bocker/ renders strip, grid and rail and stops — so
       there is no page-wide block below for the grid's right edge to answer to, and the edge that IS
       there to answer to is the campaign banner directly above it.

       So: column 1, the same track the strip is in, and the rail runs down beside both.

       AND A LANDING DOES CARRY SHELVES — /spel/ does (asked for, 2026-08-19). The paragraph above
       named this the edge to re-examine when that happened, and the re-examination went the other
       way: the SHELVES follow the ad grid rather than the grid following the shelves, because the
       block the reader compares the grid against is the campaign banner directly above it and the
       rail runs down the whole page beside both. That half is the rule directly below, and it is
       keyed the same way this one is — see it for why it is NOT in the .media-games block at the
       end of this breakpoint, where it spent its first twelve days. */
    .start-columns:has(> .start-aside > .start-puff) > .start-bands { grid-column: 1; }

    /* THE SHELVES ARE THE AD GRID'S WIDTH, NOT THE PAGE'S (asked for, 2026-08-19: the Switch shelf
       has to be the same width as the band above it).

       ON / THE ARGUMENT RUNS THE OTHER WAY and is unchanged: there the rail is a narrow block in
       row 1 only, the ad grid spans both columns beneath it, and the shelves span the page — three
       blocks ending on one line, which is what every note in this breakpoint is defending. HERE the
       rail is column 2 for the FULL HEIGHT of the page's editorial content, so there is no line at
       the page's right edge for a shelf to end on: a page-wide shelf runs under the rail's column
       and past the grid above it, and the reader sees one block sticking out rather than a column.

       A width and not a grid: the shelves are siblings of .start-columns (Start.cshtml renders them
       after it, one per carousel tab), so the only way to hand them a column's width is to compute
       it — the page minus the rail's track minus the grid's column gap, which is .start-columns'
       own --sp-5 (its ROW gap is overridden to --sp-1 further up; the column gap is not). The track
       is the shared --rail-track, so this edge follows the layout if it is ever retuned.

       Left-aligned by construction, which is what makes it line up rather than merely match: a grid
       item with a definite width cannot stretch, so it is placed at the start of .start-page's
       single column — the same page gutter the ad grid's left edge sits on, the rail being on the
       right on this landing.

       Guarded on the puff rail and NOTHING ELSE, which is where this rule parts company with the
       .media-games block it shipped in (2026-08-19 -> 2026-08-31, asked for: Böcker, Ljudböcker,
       Filmer and Musik have to match too). That block is keyed on the class because SPACING is one
       shop's judgement — five editors fill five different slots, and a gap judged by eye on one
       landing is not evidence about the other four. This is not spacing. Every sentence above it is
       a fact about the mirrored LAYOUT: the rail is column 2 for the full height, so there is no
       line at the page's right edge for a shelf to end on — true of all five landings by
       construction, exactly like "the rail is on the right" and "the ad grid is column 1" two rules
       up. It sat under a class only because /spel/ was the landing that grew shelves first, which
       is a fact about the editors rather than about the layout.

       The rail guard stays and does its own job: the WIDTH is only right while the rail is beside
       the grid, so with no rail .start-columns collapses to one column and a narrowed shelf would
       be the odd block instead. A landing whose editor has filled no puffs is therefore a no-op,
       and so is / , whose aside holds shortcuts rather than puffs. */
    .start-page:has(> .start-columns > .start-aside > .start-puff) > .start-shelf {
        width: calc(100% - var(--rail-track) - var(--sp-5));
    }

    /* AND THE RAIL SPANS THE WHOLE COLUMN BESIDE THEM, so every puff fits in it (asked for,
       2026-08-18: NYA NOVELLER belongs under NYHETER). The base rule above pins the rail to row 1 —
       the campaign slide's row, to the pixel — and pays for it with a flex column, a clip and a
       dropped blurb, because ~330px is less than one puff's natural height. That whole arrangement is
       the shortcuts rail's and it does not transfer: two puffs need the height of the strip AND the ad
       grid, which is exactly what column 1 now is.

       `grid-row: 1 / 4` and not `1 / -1`: the rows here are implicit (only the columns are templated),
       and -1 is the last line of the EXPLICIT grid, which is line 1 — the same trap recorded on the
       old .start-main wrapper. Three is the count of rows this grid ever lays: the strip subgrids into
       1 and 2, the ad grid is 3.

       `align-self: start`, undoing the base rule's stretch: the rail is now the shorter of the two
       columns, so stretching it would hand it whatever height the ad grid has and leave its last puff
       floating in the middle of a tall box. `display: grid` undoes the flex column for the same
       reason — nothing in here is negotiating for a fixed height any more, the puffs are simply
       stacked at their natural size — and `overflow: visible` undoes the clip that enforced it.

       It fills the track rather than sitting at --rail-card: that width was derived from the banner
       arithmetic in the shortcuts rail (three 842x314 uploads sharing a slide's height) and means
       nothing here, and a puff with a cover, a title, an author, a price and a blurb wants every pixel
       of the 15rem. Stretching also puts its right edge on the page's, which is what "on the right
       side" has to mean when the block below it is not page-wide. */
    .start-columns:has(> .start-aside > .start-puff) > .start-aside {
        grid-column: 2;
        grid-row: 1 / 4;
        justify-self: stretch;
        align-self: start;
        width: auto;
        display: grid;
        align-content: start;
        gap: var(--sp-3);
        overflow: visible;
    }

    /* ------------------------------------------------------------------------------------------
       /spel/ ONLY (asked for, 2026-08-19, after the first cut of this block moved every media
       landing). Everything below is one page's tuning, keyed on the class the page model now emits
       (StartDisplay.MediaClass), and it is worth saying why the key changed rather than the rules.

       The mirrored-landing rules above are keyed on :has(> .start-aside > .start-puff), and that is
       the right key for what they say: a puff rail IS the media layout, so "the rail is on the
       right" and "the ad grid is column 1" are true of all five landings by construction. Spacing
       is not like that. The five are five shops with five editors filling different slots —
       /spel/ has three bare banners and a carousel today, /ljudbocker/ has neither of those things
       today and may have both tomorrow — so a gap judged by eye on one landing is not evidence
       about the other four, and :has() cannot tell them apart because they are structurally alike.
       Hence the class: a rule that means one shop says so in its selector.

       AND HENCE WHAT LEFT (2026-08-31). The shelf-width rule was written here and did not belong:
       it says the shelves follow the ad grid because the rail is column 2 for the FULL HEIGHT of
       the editorial content, which is layout and is true of all five. It is up with the mirrored-
       landing rules now, on the puff-rail key alone. What is left below is the two rules that
       really are one landing's measurement — the strip list's align-self and the rail taken out
       of flow — and the test for anything added here is the one this block was opened with:
       could the number be judged by eye on /spel/ and be wrong on /musik/?
       ------------------------------------------------------------------------------------------ */

    /* AND THE SPACING IS THE START PAGE'S, NOT ITS OWN (asked for, 2026-08-19: the space between
       the objects should be like / ). What stands here is therefore NOTHING — no margin, no inset
       and no gap of this landing's own — and that emptiness is the finding, so here is what it
       replaces.

       Four rounds of tightening (--sp-5 -> --sp-3 -> 0 -> negative at the strip join, and the whole
       --sp-8 page gap off at the shelf join, with both blocks' insets cut to --sp-2 on the way) were
       chasing ONE fault, and it was never a spacing fault: the rail is `grid-row: 1 / 4`, so its
       overhang was being distributed across the rows the left column's blocks sit in (see the rail
       rule below). Every gap on the page was that distribution PLUS a margin, which is exactly why
       closing a margin never produced the number it was set to. With the rail out of flow the base
       spacing measures what it claims, and it is / 's:

         slide -> dots      4px   (--sp-1, the strip's own gutter, pinned for the subgrid)
         dots  -> ad grid  24px   (--sp-5, the base .start-columns > .start-bands margin)
         grid  -> shelf    48px   (--sp-8, .start-page's gap)
         shelf -> shelf    48px

       Measured on both landings at 1440. So the two differ in LAYOUT — the rail is on the right,
       the ad grid is a column rather than the page's width, the shelves follow the ad grid — and
       not in RHYTHM. That is the right split: the layout is what a puff rail forces, the rhythm
       belongs to the design system and is not one landing's to retune.

       Everything left in this block is layout. If a spacing rule is ever added back here, check
       first whether the space being complained about is a margin at all. */

    /* AND THE SLIDE'S WHITE GROUND ENDS WHERE THE PICTURE DOES (asked for, 2026-08-19: the image is
       smaller than the slide, so shrink the slide's background).

       WHERE THE SLACK COMES FROM, because it is not the slide's own doing and no amount of tuning
       .strip-panel would have found it: the rail is placed `grid-row: 1 / 4` on this landing, so
       when its puffs are taller than slide + dots + ad grid, the grid hands the surplus to the ROWS
       IT SPANS — row 1 included. The strip subgrids those rows, its list stretches into the grown
       one, and a 294px banner is then sitting in a 391px white box with 62px of --c-surface above
       it. Measured on tst, two 746x256 uploads: panel 856x391, image 856x294, label 35.

       `align-self: start` on the LIST, not on the panel: the list carries the background (it is the
       ground the page-turn happens over, see .start-strip--show .strip-list) and the panels are one
       stacked grid cell inside it, so shrinking a sheet would leave the white behind it. Sized to
       content, the surplus is bare page instead — which is what the rail's own overhang already is
       below the ad grid.

       Nothing about the slide changes size: the picture is still `width: 100%` of the same 856px
       track (asked for, 2026-08-19: do not change kampanj). This removes ground, not artwork. */
    .media-games > .start-columns > .start-strip > .strip-list { align-self: start; }

    /* AND THE RAIL STOPS SETTING THE LEFT COLUMN'S ROWS, which is the other half of the same fault
       and the bigger one. `align-self: start` above stops the LIST stretching into a grown row; it
       does not stop the row growing, so the surplus simply moved — measured after that fix: 167px
       of bare page between the slide and the ad grid, 69px between the ad grid and the shelf, and
       the reader sees a page with holes in it.

       THE MECHANISM, worth stating because nothing about it is visible in the rules it breaks: a
       grid item that spans several auto rows distributes its surplus height across the rows it
       spans. The rail is `grid-row: 1 / 4` and its two puffs are ~756px against ~555px of slide +
       dots + ad grid, so every one of those rows was carrying a third of a 200px overhang that
       belongs to a block in the OTHER column. Every gap on this landing was therefore a rail
       measurement, and no amount of tuning the margins between the left column's blocks could
       reach it — which is exactly what the four rounds of tightening above ran into.

       Out of flow, so it contributes nothing to any track: the tracks stay 1fr + --rail-track (the
       slide is the same 856px — asked for, 2026-08-19: do not change kampanj), the rail keeps its
       own width and its own top, and its overhang now hangs down the page's right-hand strip beside
       the first shelf, which is 856 of the 1120 wide and leaves that strip empty anyway. That is
       what "the rail runs down beside the editorial column" was always supposed to look like.

       The trade, stated because it is real: an absolutely positioned block cannot lengthen its
       container, so a landing whose rail is taller than everything beside it would run past the
       page. This one is not close — the rail is ~756 against a column that is over 1000 with the
       Switch shelf in it — and it is scoped to this landing precisely so that stays a fact about
       /spel/ rather than a promise about all five. Revisit it if the puff rail ever grows a third
       card, and note the structural alternative: shelves rendered INSIDE .start-columns would make
       the left column tall enough that the rail has no surplus to distribute at all. */
    .media-games > .start-columns { position: relative; }
    .media-games > .start-columns > .start-aside {
        position: absolute;
        top: 0;
        right: 0;
        width: var(--rail-track);
    }

    /* THE TILES DIVIDE THE STRIP'S HEIGHT BETWEEN THEM — no cap, no measured number (asked for,
       2026-08-18: the rail's height has to be the strip's, and the pictures adjust to fit).

       This was a max-height for two turns, first 4rem and then 2.5rem, each derived by measuring the
       uploads and adding up the tiles. Both were wrong in the same way: a constant computed from
       today's content is a guess about tomorrow's. The rail holds whatever the editor puts in it — a
       fifth tile, a longer link list, an upload of another shape — and every one of those changes the
       arithmetic while the number stays where it was.

       Flex does the arithmetic instead, and it does it in the right direction:

         - the rail is stretched to row 1, whose height the STRIP sets (its uploads are 746x256, ~294px
           at the main column, ~340px with the label and dots);
         - the links tile is `flex: 0 0 auto` — text, and the floor: squeezing it wraps "BTJ-häftet mm."
           and makes the rail taller, which is the opposite of the ask;
         - the three picture tiles are `flex: 1 1 0` with `min-height: 0`, so they share what is left in
           equal parts, whatever that is.

       `min-height: 0` is the load-bearing word. A flex item's floor is its content's min-content height
       — for an image tile that is the picture's own height — and without this the tiles would refuse to
       shrink below it, the rail would set the row again and the strip would gain blank ground under it.
       It is also what keeps the rail OUT of the row-height calculation: with a floor of zero the row is
       the strip's height and the tiles fit into it, rather than the two negotiating.

       The pictures then fill their tile (`height: 100%`) with `object-fit: contain` and their own ratio,
       so nothing is cropped or stretched — the same rule every other authored image on this page keeps.
       They are only ever drawn smaller than their upload here, never larger. */
    .start-columns > .start-aside {
        /* The one width every card in the rail takes — see .start-aside > .start-cell below for how
           it is derived and what happens when it stops being right. */
        --rail-card: 183px;
        display: flex;
        flex-direction: column;
        /* --sp-1, not --sp-2: three gaps at 8px is 24px of the rail, and every pixel of it is a pixel
           the banners cannot be wide with — see the links rule below for the arithmetic this is part
           of. The tiles are frameless pictures on a tinted ground, so a tighter gap between them
           reads as one block rather than as crowding. */
        gap: var(--sp-1);
        /* The rail is now a box of a height it did not choose, so anything that still will not fit is
           clipped rather than allowed to push the row open again. Nothing should reach this — the
           tiles below shrink and the links tile is smaller than the strip — but a fifth tile added by
           an editor would otherwise silently undo the whole arrangement. */
        overflow: hidden;
    }

    /* THE SHORTCUT LIST GOES HORIZONTAL, AND IT IS WHAT PAYS FOR THE PICTURES (asked for,
       2026-08-18). The arithmetic, measured: the rail is the slide's 328.6 tall; take off its own
       16px of padding and the gaps between four tiles and the three banners share what is left. With
       the links stacked one per line that tile is 132.2px, which leaves 52.1 per banner — and a
       842x314 upload has to be 80.6 tall to be the tile's full 216 wide. Short by 85, and the links
       tile is the only place in the rail that height can come from.

       Wrapped into rows it is ~60px, which is almost exactly the 85. So this is not a style
       preference: a vertical list and full-width banners cannot both fit a rail that is the slide's
       height, and the list is the part that loses nothing by wrapping — four short labels read as a
       shortcut bar either way, and no word is hidden or truncated.

       The note on .start-aside above still holds and is now the other way round: the links tile was
       the FLOOR that stopped the rail narrowing past ~10rem, because squeezing it wrapped
       "BTJ-häftet mm." and made the rail taller. Wrapping is deliberate here and the rail's width is
       not changing, so the floor is not being crossed — it is being spent. */
    /* ONE LABEL PER LINE — the wrapped row was tried and refused (asked for, 2026-08-18: "not
       structured properly"), and the measurement says it could never have come out even. The four
       labels are 69, 100, 98 and 113px wide in a 198px list, so a wrapped row breaks 2 / 1 / 1, and
       two even columns would be 93px, which three of the four labels overflow. There is no
       arrangement of these strings at this width that is not ragged except the column the base rule
       already draws — so this keeps `display: grid` and only tightens the leading.

       It costs the banners ~3px of height each (the tile goes ~96 -> ~106), which is the trade made
       deliberately: a shortcut list that reads as a list is worth more than 10px of banner. */
    .start-aside .cell-links {
        /* On the list, not on the <a>: the anchor is inline inside its <li>, so the row's height is
           set by the li's own strut and a line-height on the link left the pitch exactly where it was
           (measured: 29px either way). 1.4 rather than the body's 1.5 — four link labels are not a
           paragraph, and this is the part of the squeeze that cost nothing legible. */
        line-height: 1.4;
        /* No row gap: the leading already separates four one-line labels, and the 12px this returns
           is 12px the banners spend on width. The list still reads as four rows because each is its
           own line — a gap between them was decoration. */
        row-gap: 0;
    }

    /* Three rows is the floor, not two — measured, the four labels are 69, 100, 98 and 113px wide in
       a 198px list, so the last pair cannot share a line at any gap. What is left to give is the
       LEADING: the body's 1.5 is set for reading paragraphs and these are four link labels, so 1.25
       takes ~32px out of the tile without touching a word or a font size. With the tighter padding
       that is the last of the ~85px the banners needed. */
    /* Back to an even --sp-2 for the same reason as the leading above: --sp-1 top and bottom put the
       first label hard against the tile's edge. (The card was briefly made frameless to buy the
       banners another 19px of width; that was a misread of "remove the highlight", which meant the
       rail's ground, and it is reverted.) */
    .start-aside > .start-cell:has(> .cell-links) {
        padding: var(--sp-2);
        /* NO GREY LINE (asked for, 2026-08-18). .start-cell draws a 1px --c-border (#c8c4c2) on all
           four sides, which is a soft line on a pale page and a light line on a dark field — the rail
           went --c-accent under it and the border stopped reading as an edge and started reading as a
           highlight. The three banner tiles beside it have never had one (they are frameless), so the
           card was the only outlined thing left in the rail.

           Widths rather than `border: 0`, and only three of the four: the 4px top rule is the tile's
           ink accent from .ink-red, which is --c-accent — the ground's own colour, so on this rail it
           is invisible anyway, and it returns on its own if the ground ever goes pale again. */
        border-inline-width: 0;
        border-bottom-width: 0;
    }

    /* EVERY CARD IN THE RAIL IS ONE WIDTH, AND THE PICTURES FILL IT (asked for, 2026-08-18: make the
       banners as wide as the Genvägar box). The two are the same instruction: a banner is only ever
       as wide as its tile lets it be, so "match the box" is settled by giving the box and the banner
       the same number and letting the picture be width-driven instead of height-driven.

       Why the number is measured rather than 100%. The rail's height is the campaign slide's and
       cannot grow, so the width is not free: a 842x314 upload drawn W wide is W/2.682 tall, and three
       of them plus the shortcuts card have to fit what the slide leaves. At the tile's full 216 that
       is 242px of picture against 178px of room — the deficit that made every earlier attempt at this
       letterbox the artwork instead. --rail-card is the W where the two sides balance, which is why
       it is stated once, at the top of the rail, and everything reads it.

       It degrades rather than breaks if the arithmetic moves (a taller campaign upload, a fifth tile,
       an editor's longer link label): `flex: 0 1 auto` with `min-height: 0` lets the tiles shrink
       below the ratio, and `object-fit: contain` then letterboxes exactly as before — the alignment
       is what is lost, not the layout. */
    .start-aside > .start-cell {
        width: var(--rail-card);
        max-width: 100%;
    }

    /* AND THE BLEND COMES OFF ON THE DARK GROUND. .start-cell:has(> .cell-image:only-child) >
       .cell-image multiplies, which is what sinks an upload's baked-in white margins into a PALE
       region — the whole point being that white times a pale tint is that tint. Against --c-accent
       the same operation is white times a dark teal, which is the dark teal: every banner would be
       stained flat green, and the artwork inside it with it. Multiply is a rule about a light ground,
       so it belongs to the ground and not to the tile.

       What it costs is exactly what multiply was hiding: a banner whose upload carries white to its
       edges now reads as a white card on the green. That is the honest trade of a dark region, and it
       is the better failure — a picture in a frame beats a picture that has been recoloured. */
    .start-aside > .start-cell:has(> .cell-image:only-child) > .cell-image {
        mix-blend-mode: normal;
    }

    .start-aside > .start-cell:has(> .cell-image:only-child) {
        /* Still `1 1 0`, not `0 1 auto`. With an auto basis the tiles report their content height when
           the outer row is measured, and the rail — not the slide — set that row: the campaign slide
           grew 5.7px to match a 175px card, which is the one thing this whole arrangement exists to
           prevent. A zero basis keeps the rail out of the measurement entirely, so --rail-card is
           chosen to FIT the share rather than to demand it. */
        flex: 1 1 0;
        min-height: 0;
    }

    /* THE RAIL PUFF IS NOW DRAWN AT ITS NATURAL SIZE, and the five rules that used to squeeze it are
       gone (2026-08-18). Kept as a paragraph rather than deleted silently, because what they were
       compensating for is the thing that changed and the arithmetic is the argument for not putting
       them back.
       They existed to fit ONE puff into the campaign slide's row: the puff was `flex: 1 1 0` with
       `grid-template-rows: auto minmax(0, 1fr) auto`, its cover stretched to the leftover row with the
       picture contained inside it, its blurb `display: none` and its title clamped to two lines. The
       arithmetic that forced all of it, in a 15rem column against a ~328px slide: heading 17 + title
       60 + author 21 + price 21 + blurb 88 + link 21, plus padding and five gaps, is ~317px before a
       single pixel of cover — so the blurb bought the cover its ~96px and the clamp bought 20 more.
       The rail is no longer one row tall. It spans the strip AND the ad grid (the rule above), so
       there is nothing to fit into and nothing to pay with: the puffs stack at the size .start-puff
       draws them, cover centred at 8rem, blurb clamped to the base rule's four lines. The one thing
       worth re-reading here if the layout moves again is that the cover was never given a fixed
       height in the squeezed version either — it took what was left — so restoring these rules means
       restoring the whole set, not one of them. */

    /* AND THE BOX IS THE WHOLE TILE (asked for, 2026-08-18: the banners have to fit the tile, wider
       and centred). `align-self: stretch` is the load-bearing word and it is undoing a base rule that
       is right where it was written: .cell-image is `align-self: start` so that, as a grid item
       spanning an ad-cell's rows, it does not grow to the cell's height and frame the picture in
       empty ground. Here the cell is a flex COLUMN, and start-aligning across it means the anchor
       shrink-wraps the picture — so `width: 100%` on the <img> below was 100% of a 140px box that was
       itself sized by the picture. A circle: the tile was 216 wide and the picture could never be
       told about it. */
    .start-aside > .start-cell:has(> .cell-image:only-child) > .cell-image {
        display: block;
        align-self: stretch;
        width: 100%;
        height: 100%;
        min-height: 0;
    }

    /* `height: auto`, not 100%: the picture is the width's consequence now (see --rail-card). With a
       fixed height and `contain` the box was the tile and the artwork floated inside it letterboxed,
       which is what "not as wide as the box" looked like. `contain` stays for the degraded case the
       rule above describes. */
    .start-aside .cell-image img,
    .start-aside img.cell-image {
        width: 100%;
        height: auto;
        max-height: 100%;
        object-fit: contain;
    }
}

/* Motion is decoration here — the cell lift and the shelf card's tone change both go away when
   asked. The cover no longer moves at all (it stands on a tint now), so it is off this list.

   The slideshow itself is deliberately NOT gated here — that it advances, and the fill that says when,
   both stay. The preference is off by default on a great many managed Windows desktops, which is what
   this shop's callers sit at, so gating the feature on it meant the feature did not exist.

   The dissolve is part of that "not gated", and it was briefly listed here anyway — see the note inside
   the block for what that looked like. What belongs here is decoration that carries no information. */
@media (prefers-reduced-motion: reduce) {
    .start-cell,
    .shelf-card { transition: none; }

    /* THE DISSOLVE IS NOT LISTED HERE, and the rule that keeps it out is written three times in this
       file already: the slideshow is not gated on this preference. "Animation effects" is off by
       default on a great many managed Windows desktops — which is what this shop's callers sit at —
       and browsers pass that straight through as `reduce`, so anything switched off here is switched
       off for most of the people who will ever see it.

       It WAS listed here for one turn, at the slideshow's own specificity so it would actually bite,
       and it bit: the slides changed with no animation at all and the feature looked broken. That is
       the whole argument in one observation — a preference that is off by default is not a minority
       setting, and putting a feature behind it deletes the feature. What the preference is owed is
       something that stops the movement outright, and that is hover, focus and a hidden tab (start.js
       running()), not a switch that turns the change into a cut nobody asked for.

       The fill STAYS for the same reason and a stronger one: it is a countdown, not decoration. It says
       how long is left before the page changes under the reader, which is information this caller needs
       MORE than most, not less. Nothing about it sweeps across the viewport or parallaxes — it is a
       48px bar filling once over five seconds. What goes here is the dot's hover tone change, which
       carries nothing. */
    .strip-dot { transition: none; }

    /* AND THE DISSOLVE IS PUT BACK, against the shop-wide reset. btj.css:348 answers this preference
       the way every accessibility checklist says to — `*, *::before, *::after { transition-duration:
       0.01ms !important }` — so the slideshow's transition was 0.01ms no matter what this file said,
       and nothing local could win: the reset carries !important on the universal selector, which is
       the strongest thing CSS has short of another !important at higher specificity. That is what
       "no animation" was, twice over.

       So this is the second !important, and it is deliberately the narrowest one that can beat it:
       the duration only, on the two selectors that draw the slide change, in the slice's own sheet.

       WHY THE SLIDESHOW GETS AN EXEMPTION AT ALL. On this shop's callers — managed Windows desktops —
       "Animation effects" is off by DEFAULT, and browsers pass that through as `reduce`. The global
       reset is therefore not a minority accommodation here; it is the majority setting, and a feature
       switched off by it is a feature that does not exist for most people who will ever see it. The
       preference deserves an answer, but the honest answer for auto-advancing content is a way to STOP
       it, which this has (hover, focus, a hidden tab — start.js running()), not the removal of the
       700ms that tells the reader one banner became another. Everything the reset switches off
       elsewhere on this page stays off: the cell lift, the card tone change, the dot's hover.

       If this ever needs revisiting, the question is about btj.css and not about this rule: a global
       reset that fires for most of the shop's callers is worth a second look on its own terms. */
    .start-strip--show .strip-panel,
    .start-strip--show .strip-panel[data-current],
    .start-strip--show .strip-panel[data-leaving] {
        transition-duration: var(--fade-ms) !important;
    }
}
