/* ==========================================================================
   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-campaigns {
    display: grid;
    align-content: start;
    gap: var(--sp-5);
    min-width: 0;
}
.start-campaigns > .start-strip { min-width: 0; }
.start-campaigns > .start-bands { padding-block: 0; min-width: 0; }

/* 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. */
/* Each desktop column owns its vertical flow. A tall book card must not
   enlarge the carousel row or the space above the campaign banners. */
@media (min-width: 62rem) {
    .start-page { --rail-track: 15rem; }

    .start-columns {
        grid-template-columns: minmax(0, 1fr);
        align-items: start;
    }
    .start-columns:has(> .start-aside) {
        grid-template-columns: minmax(0, 1fr) var(--rail-track);
    }
    .start-columns > .start-aside {
        grid-column: 2;
        align-self: start;
        min-width: 0;
        gap: var(--sp-3);
    }
    .start-columns:not(:has(> .start-campaigns)) > .start-aside {
        grid-column: 2;
    }
    .start-page:has(> .start-columns > .start-aside > .start-puff) {
        row-gap: var(--sp-5);
    }
    .start-page:has(> .start-columns > .start-aside > .start-puff) > .start-shelf {
        width: calc(100% - var(--rail-track) - var(--sp-5));
    }
}

/* 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;
    }
}
