/* Fergus Oswald — v3.
 *
 * Built from mockup/v3/source.dc.html, 19 Sep 2026. The design was made in
 * Claude with every style inline; this pulls them into classes so the same
 * rule is written once. Where a number here looks arbitrary it came from the
 * mockup — the clamp() ranges especially.
 *
 * ## The thing that shapes everything
 *
 * The page does not scroll. The shell is exactly the viewport, and only the
 * left pane scrolls inside it. That is the design: header, panel, index, with
 * a photograph filling the right. It means every panel has to survive a short
 * window, which is why the type scales with the viewport height as well as
 * its width.
 */

:root {
  --paper:  #F2E3C7;   /* the ground. Warm cream */
  --ink:    #33200F;   /* the text. Near-black brown */
  --flame:  #D9541F;   /* the accent: hover, the one coloured word */
  --deep:   #9C3A10;   /* a link that has been hovered */
  --pencil: #4A3A2C;   /* the handwriting and the crayon underlines */
  --dim:    rgba(51, 32, 15, 0.76);
  --faint:  rgba(51, 32, 15, 0.22);
  --wash:   rgba(51, 32, 15, 0.06);

  /* The ground the mobile menu is painted on. A token because the page has two
     grounds — cream, and the dark one under the photographs — and the menu is
     opaque, so it has to bring whichever one is in force with it. */
  --menu-bg: #F2E3C7;

  /* ⚠️ THE DARK GROUND, NAMED — 26 Sep 2026. Nick: *"the colouring is slightly
     different. Look at that."* It was: the home page had arrived from a design
     refresh carrying #201711, #f1e3c9 and #dc6439, against the site's own
     #2A1E14, #F2E3C7 and #D9541F. Three near-misses, each close enough that
     nobody would name the colour and far enough that the seam showed.

     ⚠️ THE SITE'S VALUES WON, NOT THE REFRESH'S, and the difference is
     invisible either way — what matters is that there is now ONE of each.
     These already drive the record label, the crayon underlines and the
     hand-drawn boxes, so taking the refresh's values instead would have
     recoloured half the site to settle a difference nobody can see. */
  --ground: #2A1E14;

  --display: 'Bricolage Grotesque', Helvetica, Arial, sans-serif;
  --hand:    'Architects Daughter', 'Comic Sans MS', cursive;
  --body:    Karla, Helvetica, Arial, sans-serif;

  /* ⚠️ THE FOURTH FACE, from 26 Sep 2026, and it is the HEADER'S face — the
     wordmark, the tagline and the index, on every page.

     Nick: *"Make the navigation and the rest of the site look like it does on
     the homepage."* The home page arrived from a design refresh set in
     Georgia, and rather than pull it back into the other three faces he chose
     the other direction: the chrome is Georgia everywhere now.

     ⚠️ IT IS A TOKEN AND NOT THREE COPIES OF 'Georgia, serif'. The header is
     the one part of this site that appears on every page, so a face written
     out in each rule is three chances to drift.

     ⚠️ AND IT STOPS AT THE CHROME. The panels keep the hand, the display face
     and Karla — the crayon headings, the drawn boxes and the record label are
     the site's character and none of them is in Georgia. This token is the
     frame, not the picture. */
  --mark:    Georgia, 'Times New Roman', serif;
}

html, body {
  margin: 0; padding: 0; height: 100%;
  background: var(--paper);
  overflow: hidden;
}
* { box-sizing: border-box; }
body { font-family: var(--body); color: var(--ink); }

a { color: var(--ink); text-decoration: none; }
::selection { background: var(--flame); color: #F2E3C7; }
/* ⚠️ THE TOKEN, not the brown. Typed literally, this survived the flip and
   left the contact fields as three empty boxes on the dark — no idea what went
   in any of them. Same trap as .hard-fail, in the opposite direction: that one
   paired two tokens that had become one colour, this one dodged the tokens
   altogether. textarea too; only inputs were covered. */
input::placeholder, textarea::placeholder { color: var(--dim); opacity: 0.8; }
.defs { position: absolute; }

:focus-visible { outline: 2px solid var(--flame); outline-offset: 2px; }

@keyframes fadein  { from { opacity: 0; } to { opacity: 1; } }
@keyframes slidein { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes draw     { from { stroke-dashoffset: -62; } to { stroke-dashoffset: 0; } }
/* The box round a button. pathLength="100" normalises the outline, so this is
   the same arithmetic whatever width the button turns out to be. */
@keyframes draw-box { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }
/* ⚠️ `pale` IS THE OLD BEHAVIOUR AND IS NO LONGER USED ON THE SIGNUP BUTTON.
   It is kept because it is the readable record of what the button did until
   23 Sep 2026: dark pencil strokes laid down, and the word going pale as they
   arrived underneath it. */
@keyframes pale      { from { color: var(--ink); } to { color: var(--paper); } }

/* The words on the signup button go DARK as the white arrives underneath.
   Nick, 23 Sep 2026: *"make that box coloured in with the white colour, and
   make the 'Sign up' word dark so that it can be seen."*

   ⚠️ It starts from `--ink` and not from a fixed colour, and that is the whole
   trick — it makes one keyframe right on both grounds. On the cream page
   `--ink` IS the dark brown, so the word is dark before the fill and dark
   after it: nothing visibly happens, which is correct, because a dark word on
   cream was always readable. On a photo home page `--ink` is flipped to the
   paper colour, so the word starts pale against the photograph and turns dark
   exactly as the white block arrives to carry it. */
@keyframes ink       { from { color: var(--ink); } to { color: #33200F; } }
@keyframes write   { from { clip-path: inset(0 100% -20% 0); } to { clip-path: inset(0 -6% -20% 0); } }

/* The mockup animates for thirteen seconds. Honoured, but never at the cost
   of the content appearing — every animated thing is visible at rest. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001s !important;
    animation-delay: 0s !important;
    transition-duration: 0.001s !important;
  }
  /* ⚠️ Not enough on its own for the doors. Both of their animations START
     from an invisible state, so merely making them instant leaves a page of
     blank boxes for a moment — and on a slow machine, longer. These land them
     finished. */
  .door-word, .written-word, .note, .ask-kicker, .ask-words span, .ask-small,
  .ask-go .go-word { clip-path: none !important; }
  .door-box path, .drawn-box path, .drawn-box line,
  .drawn-rule path { stroke-dashoffset: 0 !important; }
  /* ⚠️ REVERSED 23 Sep 2026 with the button itself. The fill is white now, so
     a word that lands pale lands white on white. It has to land DARK. */
  .ask-go .go-word { color: #33200F !important; }
}

/* ---- the frame ----------------------------------------------------------- */

.shell {
  position: fixed; inset: 0;
  display: grid;
  /* ⚠️ `minmax(0, 1fr)` AND NOT `1fr`, for the same reason the rows already
     say it — and leaving the column out was a real fault, not a tidiness one.
 
     A grid with no declared column gets ONE implicit `auto` column, and a grid
     item's default `min-width: auto` refuses to shrink below its own
     min-content. So the column grew to whatever the widest thing inside
     demanded and the whole frame went with it: **measured at 452px inside a
     390px phone on 21 Sep 2026**, with `.top`, `.body` and every panel all
     452 wide and the right-hand 62px simply off the screen.
 
     That one missing word is why the standfirst read "Singer / Songwriter /
     Instrumentalis", why the index sliced "Drums etc" and dropped
     Follow/Contact onto a second line behind a stray slash, and why Nick
     reported the Releases and Drums etc pages as "too wide" on a phone. Four
     symptoms, one cause — and the suite passed through all of them, because
     nothing in it measures a width.
 
     `minmax(0, 1fr)` lets the column be narrower than its content, and the
     things inside that scroll (the reel, the lists) go back to scrolling. */
  grid-template-columns: minmax(0, 1fr);
  /* ⚠️ TWO ROWS, not three. The third was the index along the bottom; it moved
     into the header on 25 Sep 2026 and the row went with it. */
  grid-template-rows: auto minmax(0, 1fr);
  padding: clamp(14px, 2.2vh, 26px) clamp(16px, 2.6vw, 34px);
}

.top {
  /* Positioned for the same reason .pane is: so the full-bleed photograph on
     the home page passes UNDER the wordmark rather than over it. A fixed
     .plate carries z-index 0, and a positioned element with z-index 0 paints
     above ordinary in-flow content — so without this the picture would sit on
     top of the name. */
  /* ⚠️ z-index 3, not 1. The header now holds the mobile menu, which drops
     DOWN over the panel — and the panel is z-index 1. At 1 the menu opened
     behind the page it was meant to cover. */
  position: relative; z-index: 3;
  padding: clamp(14px, 3.2vh, 40px) 0 clamp(10px, 1.6vh, 18px);
  border-bottom: 1px solid var(--ink);
}
/* ⚠️ THE HEADER IS ONE ROW ON EVERY PAGE — 26 Sep 2026, the arrangement the
   home page already had. Nick: *"Make the navigation and the rest of the site
   look like it does on the homepage."*

   It used to be two stacked rows — name and tagline across the top, index
   underneath — which is a masthead. Home put the name left and the five words
   hard right on one line, and that is the shape now.

   ⚠️ SO THE TAGLINE HAD TO MOVE, and there was nowhere obvious. Top-right is
   where the index goes now. It sits under the name instead, which is where a
   tagline belongs anyway — a strapline beside a wordmark was always competing
   with it.

   ⚠️ AND IT BECOMES ONE LINE. Four stacked words under the name made the
   header twice as tall as the home page's; the `<br>`s are turned into spaces
   rather than removed from the markup, because the phone still stacks them. */
.top {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: clamp(18px, 3vw, 46px); flex-wrap: wrap;
}
/* ⚠️ `flex: 1 1 auto` AND `min-width: 0`, NOT `flex: 0 0 auto` — and this is
   the SAME FAULT the shell's grid column documents at length further up, met a
   second time in a different property.

   `0 0 auto` refuses to shrink, so this column took the full width of the
   tagline on one line — "singer · songwriter · instrumentalist · edinburgh,
   leeds" — and pushed `.top` to 484px inside a 390px phone. **The burger went
   with it, to x=450, off the side of the screen**: no menu button at all on a
   phone, and nothing in the suite says a word because nothing measures a
   width.

   Found by rendering at 390px and printing the element's rectangle, which is
   the only thing that could have found it. It is invisible in the source and
   invisible in a screenshot — the button is simply not there. */
.top-row {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  flex: 1 1 auto; min-width: 0;
}
.top > .index { flex: 0 1 auto; justify-content: flex-end; padding-top: 0; }

/* ⚠️ THE BREAKS BECOME SPACES, NOT NOTHING. `display: none` on a `<br>` closes
   the words up — the same fault that made the wordmark read "fergusoswald".
   An inline-block with a space in it keeps them apart. */
/* ⚠️ The `<br>` trick that used to be here is gone: a `<br>` set to `display:
   inline` still breaks the line, and `display: none` closes the words up. The
   separators are real text in `index.html` now. */
/* ---- the hamburger ------------------------------------------------------
   ⚠️ Three lines drawn in CSS, not an icon font and not an SVG. It is the one
   piece of chrome on this site that is NOT hand-drawn, on purpose: a wobbly
   hamburger reads as a mistake rather than as a style, because everybody knows
   exactly what shape this is meant to be. */
.burger {
  display: none;                         /* desktop has the index itself */
  flex-direction: column; justify-content: center; gap: 5px;
  /* ⚠️ NO HORIZONTAL PADDING and no negative margin. The first version had
     `padding: 0 8px; margin-right: -8px` to sit the bars flush with the rule
     below — and the negative margin pushed the button 8px past the row it was
     in, so on a 390px phone the right-hand bar ends were cut off by the screen
     edge. The bars ARE the button now; it is the whole width that lines up.

     44px tall for the thumb, pulled back with a negative bottom margin so it
     does not set the header's height. */
  width: 34px; height: 44px; flex: 0 0 auto;
  margin: 0 0 -2px 0; padding: 0;
  border: 0; background: none; cursor: pointer;
}
.burger span {
  display: block; height: 2px; width: 100%; background: var(--ink);
  transition: transform 0.2s ease, opacity 0.15s ease;
}
/* Open, it is a cross — so the same control closes it and there is never a
   menu on screen with nothing obvious to press. */
.burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
/* ⚠️ LOWERCASE, IN THE HEADER'S FACE, ON EVERY PAGE — 26 Sep 2026. Nick:
   *"Fergus Oswald runs in the same font as elsewhere. Let's make it all
   lowercase everywhere else."*

   It was `--display` at 700 in CAPITALS, which is a different mark from the
   one on the home page — two wordmarks for one artist, decided by which page
   you happened to land on.

   ⚠️ THE LINE BREAK IS GONE WITH IT. `Fergus<br>Oswald` stacked the name in
   two lines because a heavy uppercase mark needed the width; a lowercase
   Georgia one does not, and stacked lowercase reads as two words rather than
   a name. The `<br>` is hidden everywhere now, not only on home — which is
   why the space after it in the markup is load-bearing on every page and not
   just one. */
.wordmark {
  font-family: var(--mark); font-weight: 400;
  font-size: clamp(21px, 2.3vw, 33px); line-height: 1;
  letter-spacing: -0.045em; text-transform: lowercase;
  white-space: nowrap;
}
.wordmark br { display: none; }
/* ⚠️ THE TAGLINE FOLLOWS THE WORDMARK, 26 Sep 2026 — Nick named it in the
   same breath. It was 10px letterspaced CAPITALS, which is a third voice in a
   header that now has one: an uppercase caption under a lowercase name.

   ⚠️ THE LETTERSPACING GOES TOO, and that is the part that matters. Wide
   tracking is what makes small capitals legible; keep it on lowercase italic
   and the words come apart. Removing the case without removing the spacing
   would have looked like a mistake rather than a change. */
.standfirst {
  margin: 0 0 3px;
  font-family: var(--mark); font-style: italic; font-weight: 400;
  font-size: clamp(11px, 0.95vw, 13px); line-height: 1.35;
  letter-spacing: 0; text-transform: lowercase;
  color: var(--dim);
  /* ⚠️ LEFT, from 26 Sep 2026. It was right-aligned because it sat in the
     top-right corner opposite the wordmark; it sits UNDER the wordmark now, so
     right-aligning it hung it off the wrong edge — visible immediately, and
     nothing in the suite would have said a word. */
  text-align: left;
}
/* ⚠️ THE `<br>` SHOWS AGAIN, from 26 Sep 2026. It was turned into a separator
   when the tagline went on one line; it is a two-line sentence now, so the
   break is the line break it always was. */
.standfirst br { display: inline; }

/* ⚠️ THE EMPTY DIARY ROW IS IN THE HANDWRITING, from 26 Sep 2026, and it is
   the only row on this page that is. A real gig is set in the page's body
   face because it is data — a date, a venue, a ticket link. This is Fergus
   saying there is nothing yet, so it is in his hand and not in the table's. */
.row.row-empty {
  font-family: var(--hand);
  font-size: clamp(16px, 1.7vw, 21px);
  color: var(--dim);
  padding-top: clamp(6px, 1.2vh, 12px);
}
.row.row-empty .where { font-weight: 400; }
/* ⚠️ THE ARROW IS FORCED INTO THE PAGE'S OWN TYPE. The variation selector in
   `ARROW` asks the system for the text form; naming serif fonts here is the
   belt to that braces, because a font stack that has no glyph for U+2197 will
   fall back to one that does — and the one it picks on a phone is the emoji
   font. Both are needed: the selector alone loses to a missing glyph. */
.arr { font-family: var(--mark); font-variant-emoji: text; }


/* The photograph sits BEHIND the words, not beside them.
   Nick, 19 Sep 2026: "put the video / image behind the buttons aligned to
   right and with a mask that blends the photo/video into the background
   colour so most of behind the buttons is the colour not the photo."

   So this stopped being a two-column grid. The words span the full width and
   the picture is a layer underneath, pushed to the right and faded out
   leftwards — see .plate. */
.body {
  position: relative;
  min-height: 0;
  padding: clamp(14px, 2.4vh, 30px) 0;
  display: flex;
}

.pane {
  position: relative; z-index: 1;      /* above the wash */
  flex: 1 1 auto;
  min-width: 0; min-height: 0;
  /* ⚠️ NOT `justify-content: center`. A centred flex column that is taller
     than its box overflows at BOTH ends, and the top overflow cannot be
     scrolled to — scrollTop is already 0 there. On a short window the six
     buttons sat above the pane's top edge and were unreachable by any means.
     
     `margin-block: auto` on the child centres it exactly the same way when it
     fits, and collapses to a normal top-aligned scroll when it does not. The
     brief is no scrolling; the fallback is for the window that makes that
     impossible, and a reachable page beats a clipped one. */
  display: flex; flex-direction: column; justify-content: flex-start;
  overflow-y: auto; scrollbar-width: none;
}
.pane > * { margin-block: auto; }
/* ⚠️ HOME IS CENTRED, THE SUB PAGES ARE NOT. Nick, 21 Sep 2026: "keep the
   title of each section at the top at same level". Vertical centring puts a
   short panel's heading halfway down and a long one's at the top, so the
   written heading jumps as you move between them. Only home — six doors and
   a signup, designed as one centred block — keeps the auto margins. */
.pane-top { justify-content: flex-start; }
.pane-top > * { margin-block: 0; }
.pane::-webkit-scrollbar { width: 0; height: 0; }
.panel { animation: slidein 0.45s ease both; }

/* ---- the photograph ------------------------------------------------------ */

/* The wash.
 *
 * Pinned to the right of the content area and faded out towards the left, so
 * the buttons sit on cream and the picture is only really present at the right
 * edge. Three things do that work together and all three are needed:
 *
 *   - the MASK fades the layer itself out leftwards, to nothing by 88%
 *   - `multiply` blends what is left into the cream rather than sitting on it
 *   - grayscale plus a low opacity stops a colour photograph competing with
 *     the one accent colour on the page
 *
 * A mask rather than a gradient overlay on top: an overlay would also dim the
 * text sitting above it, and the text is the point.
 */
.plate {
  position: absolute;
  top: 0; bottom: 0; right: 0;
  width: min(62%, 780px);
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to left, #000 0%, #000 34%, rgba(0,0,0,0.45) 62%, transparent 88%);
  mask-image: linear-gradient(to left, #000 0%, #000 34%, rgba(0,0,0,0.45) 62%, transparent 88%);
}
.plate img, .plate video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: right center;
  filter: grayscale(1) contrast(1.04);
  mix-blend-mode: multiply;
  opacity: 0.5;
}

/* ⚠️ THE FADE IS ON THE PARENT, NOT THE PICTURE, and that is a bug fix
   rather than tidying.
   
   `fadein` runs `opacity: 0 → 1`, and `animation-fill-mode: both` leaves the
   final keyframe in force for ever. An animation beats a normal declaration in
   the cascade, so `opacity: 0.5` on the picture above NEVER APPLIED — every
   photograph rendered at full strength.
   
   It was invisible because no photograph has ever been in the picture store, so
   the plate has only ever shown its placeholder text. The first picture put
   behind the page, on 21 Sep 2026, came out almost black and the page with it.
   
   Fading the PARENT leaves the picture's own opacity alone. */
.plate { animation: fadein 0.6s ease both; }
/* ---- the home page, with a photograph behind it --------------------------- */
/*
 * Nick, 21 Sep 2026: *"the text/buttons etc should be the beige colour instead
 * of the black/brown"*, and before that *"the photo/video to be under the
 * buttons and text… photo/video to completely fill the screen on phone."*
 *
 * So home stops being ink-on-paper and becomes paper-on-photograph. It is four
 * custom properties rather than a second set of rules, because every colour on
 * the page already comes from these — including the hand-drawn boxes, whose
 * stroke was changed to `currentColor` in `app.js` on the same day so that a
 * flip reaches them too. It used to be the hex value typed twice.
 */
/* ⚠️ SCOPED TO .pane, NOT TO THE PAGE — and it has to be.
   
   The photograph now stops at the two rules, so the header and the index sit on
   plain beige. Flipping the whole page would put beige writing on beige there:
   the wordmark and the index would vanish. Only the band over the photograph
   goes pale. Caught by rendering it; the first version flipped `body`. */
/* ⚠️ NO LONGER SCOPED TO .pane. The note above was right while the photograph
   stopped at the two rules: flipping the whole page would have put beige on
   beige in the header and the index. Now the photograph runs behind those too,
   so the opposite is true — leaving them dark would put brown writing on a
   dark picture. The flip goes on the body and reaches everything. */
/* ⚠️ SCOPED BACK TO `.pane`, 25 Sep 2026. Nick: *"can we keep the images out of
   the header and footer?"* — which is the same instruction he gave on 21 Sep,
   *"the header and the footer should be the original beige"*, asked again
   because the code had drifted back.

   The flip was moved onto `body` when the picture started running the full
   height of the window, and the reasoning then was sound: *"leaving them dark
   would put brown writing on a dark picture."* **The picture no longer runs
   the full height** — see `.plate.full`, which was `position: fixed` against a
   comment insisting it was not — so the reason has gone and the flip goes back
   with it.

   ⚠️ So the bands above and below are cream with brown writing, as they were
   drawn, and only the panel between the two rules is paper-on-photograph. */
/* ⚠️ SCOPED BACK TO THE WHOLE PAGE, 25 Sep 2026 (second change today).
   Nick: *"Make the header and footer darker - around the colour it was when
   the photo extended into it - change the type accordingly so it shows up."*

   ⚠️ THIS IS A COLOUR DECISION, NOT A REVERSAL OF THE PICTURE ONE. Earlier
   today the flip was pulled back to `.pane` because the photograph had stopped
   running the full height, and cream bands with brown writing were what he
   drew. He has now asked for the bands to be DARK anyway — the ground the
   photograph used to sit on — while the photograph itself stays between the two
   rules. Both are true at once: `.plate.full` is still absolute inside `.body`,
   and there is a check that it never goes back to `fixed`.

   So do not "restore" the `.pane` scoping by reading the history above. The
   picture is in the lines; the colour is not. */
body.photo-home,
body.photo-home .pane,
body.photo-home .plate {
  --ink:    #F2E3C7;                      /* the writing is the paper colour now */
  --pencil: #F2E3C7;
  --dim:    rgba(242, 227, 199, 0.88);
  --faint:  rgba(242, 227, 199, 0.42);
  --wash:   rgba(242, 227, 199, 0.08);   /* the film wells, now that they sit on a photograph */
  color: var(--ink);
}

/* The picture can be strong now, and has to be: pale writing needs something
   dark under it. This is the opposite problem to the cream page, where the
   photograph had to stay faint to keep out of the ink's way. */
/* ⚠️ 1, not 0.92, from 25 Sep 2026 — with the filter above, this is the other
   half of *"make it brighter across the whole site."* At 0.92 the picture was
   being faded into the near-black behind it as well as toned; there is no
   reason for both. */
/* ⚠️ The dark ground belongs to the BODY BAND, not the page. `body` keeps the
   paper colour so the header and the index sit on cream; `.body` — the band
   between the two rules — is what goes dark behind the picture. */
body.photo-home { --plate-strength: 1; --menu-bg: var(--ground); }
body.photo-home .body { background: var(--ground); }
/* The bands above and below take the same ground, so the page is one dark
   sheet with the picture in the middle of it rather than a lit strip between
   two cream ones. */
body.photo-home,
body.photo-home .shell { background: #2A1E14; }

/* ⚠️ THE BUTTON DOES NOT FOLLOW THE PAGE'S FLIP, and that is still the point —
   but it is now pinned in the STYLESHEET rather than here.

   Everything else in the band goes pale because it sits on a photograph. The
   button is not writing on a photograph — it is a coloured-in block, and it
   brings its own ground with it. From 23 Sep 2026 that block is WHITE with a
   dark word on it (Nick's request), and both of those colours are hard-coded
   on `.ask-go` near the bottom of this file, so there is nothing left for this
   block to correct.

   ⚠️ KEEP READING BEFORE CHANGING THE BUTTON. The 21 Sep version of this note
   recorded that a pale block with a dark word had been TRIED and was
   unreadable: the word was lost among the gaps between the pencil strokes,
   which on a photograph show the picture through. That failure was real. What
   fixed it was not going back to dark — it was closing the gaps: the strokes
   are laid fully opaque so the middle of the block is solid and only its edges
   stay rough. Undo that and the 21 Sep problem comes straight back.

   Caught, then as now, by cropping the render and looking at it — not by any
   check. */

/* Solid, per Nick's *"buttons to be solid"*. Over a photograph a hairline reads
   as a smudge, so the drawn OUTLINES thicken and stop being translucent.

   ⚠️ `.edge`, NOT `path` — and this one selector shipped the signup button
   broken to the live site on 23 Sep 2026.

   It said `path`, which was exactly right while the only paths inside a drawn
   box were outlines. The signup button's pencil colouring is ~48 more paths in
   the same box, and this rule was forcing every one of them from the 12px they
   are generated at down to 2.6px. On a 5px step with a filter that displaces
   points by up to 3.4px, 2.6px strokes do not touch: the white block came out
   shot through with holes showing the photograph, and the dark word sat
   invisible among them.

   ⚠️ **Nick read it on the live site: *"the signup button is still
   illegible"*.** Everything measurable was correct — the stroke attribute said
   12.12, the colour computed to white, the opacity to 1, the word to dark
   brown, and 348 checks passed. The browser was rendering 2.6px. **A generated
   attribute is not what gets drawn; the computed style is**, and nothing in
   this suite was reading computed styles.

   ⚠️ It also explains why the isolated render looked right and the live page
   did not: the test harness had no `.pane` in the ancestry, so this rule never
   applied to it. **An isolated render is not the page.** */
body.photo-home .pane .door-box .edge,
body.photo-home .pane .drawn-box .edge,
body.photo-home .pane .drawn-box line { stroke-width: 2.6; }
body.photo-home .pane .hint { opacity: 1; }

/* ⚠️ REVERSED 21 Sep 2026. This block used to hide the drawn box on the home
   page and put a flat cream rectangle behind the word instead.
   
   Nick, looking at the live site: *"the sign-up button... needs to be coloured
   in roughly, not smoothly."* That flat rectangle IS the smoothness — the
   pencil colouring was still being generated underneath and then hidden.
   
   The original reasoning was that a sketch "reads as a sketch on paper and as a
   mess on a photograph". That was a guess about how it would look, made without
   rendering it, and it was wrong: the colouring-in is the character of this
   site, and taking it off the one button everybody is asked to press took the
   character off exactly the wrong thing.
   
   What the flip actually needed was only the SECOND half of the old note — the
   email box, whose text is hard-coded brown and invisible here. That stays. */
body.photo-home .pane .ask-go { background: transparent; }

/* ⚠️ The full-opacity strokes moved to `.ask-go` itself on 23 Sep 2026 and now
   apply on BOTH grounds, because the white block needs to be solid everywhere,
   not only over a photograph. This rule is left as a pointer rather than
   deleted: it is where anyone looking for the photo-home stroke weight will
   come, and finding nothing reads as "it was never set". */

/* The small print under the form, and the standfirst, sit directly on the
   photograph with nothing behind them. A shadow rather than a panel: a box
   behind every stray line would turn the page into a set of cards. */
body.photo-home .pane { text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55); }

/* Nick, 21 Sep 2026: *"let's put some padding between the buttons and the left
   side."*
   
   On every other panel the words start at the left rule, which is right when
   they are on plain cream — the rule and the text share an edge and the page
   reads as one column. Over a photograph it reads as the buttons being pushed
   up against the frame.
   
   It goes on the PANE rather than on `.doors`, so the signup and its small
   print move with the buttons. Indenting the buttons alone would leave the
   writing beneath them hanging off to the left, which is worse than the thing
   it fixes. Scoped to the photograph page: the text panels are unchanged. */
body.photo-home .pane { padding-left: clamp(16px, 2.6vw, 44px); }

/* ---- the photograph, full bleed behind the home page ---------------------- */
/*
 * Nick, 20 Sep 2026: *"put the image completely behind the home page."*
 *
 * On every other panel the photograph is a column down the right, handed back
 * to the words by the mask above. On HOME it fills the window instead and sits
 * behind everything — the wordmark, the doors and the index alike.
 *
 * **`fixed`, not `absolute`.** `.plate` lives inside `.body`, which sits inside
 * `.shell`'s padding, so an absolute full-bleed would stop at the padding edge
 * and leave a cream margin down three sides — which reads as a mistake rather
 * than a design. Nothing in its ancestry uses `transform`, `filter` or
 * `will-change`, so `fixed` reaches the real window edges. Checked, because a
 * single transform anywhere above it would silently turn this back into an
 * absolute box and the border would come back.
 */
.plate.full {
  /* ⚠️ ABSOLUTE, INSIDE .body — not fixed to the window, and that is Nick's
     instruction of 21 Sep 2026: *"the header and the footer should be the
     original beige — the photo/video should not extend to those, it should be
     within the lines."*
     
     The lines are the rule under the wordmark and the rule over the index.
     `.body` is exactly the band between them, so `inset: 0` against it lands on
     both without a number being typed. An earlier version of this was
     `position: fixed; inset: 0`, which filled the whole window — and took the
     header and footer with it. */
  /* ⚠️ FIXED TO THE WINDOW, which reverses the 'within the lines' rule of
     21 Sep. Nick, same day, later: *"extend the background image to go behind
     the header and footer."* So it is the whole window again, and the two
     rules now sit ON the photograph rather than fencing it in.
     
     Nothing in .plate's ancestry uses transform, filter or will-change, so
     `fixed` reaches the real window edges — checked, because any one of those
     above it would silently turn this back into an absolute box and put the
     beige margin back down three sides. */
  /* ⚠️ `absolute`, NOT `fixed`. It was fixed, and the comment above — written
     when it was absolute — has been describing something the file stopped
     doing. **A stale comment is worse than no comment**, and this one hid the
     fault for days: it says in Nick's own words that the picture must not
     reach the header and the footer, directly above a rule that made it fill
     the whole window.

     Nick, 25 Sep 2026: *"can we keep the images out of the header and
     footer?"* — which is the same instruction as 21 Sep, asked again because
     the code had drifted back.

     `.body` is `position: relative` and is exactly the band between the rule
     under the wordmark and the rule over the index, so `inset: 0` against it
     lands on both without a number being typed. */
  position: absolute;
  inset: 0;
  width: auto;
  /* No mask. The mask exists to give the left half back to the words; behind
     all of them there is no left half to give back. */
  -webkit-mask-image: none;
  mask-image: none;
}

/* ⚠️ THE SCRIMS, carried over from the v4 exploration, and they are what makes
   a full-bleed photograph survive under a LIST rather than under six buttons.
   
   Home could rely on `text-shadow` alone: big pale words, lots of gaps, and the
   picture reading through them was the point. Releases, Live and Playlists are
   rows of 11px labels and hairline rules, and those disappear over any busy
   part of a photograph.
   
   Two gradients rather than one flat tint: a diagonal weighted to the left,
   where the writing lives, and a vertical one that anchors the two rules top
   and bottom. A flat tint strong enough for the tracklist would have killed the
   photograph everywhere else. */
.plate.full::before,
.plate.full::after {
  content: '';
  position: absolute; inset: 0;
  pointer-events: none;
  z-index: 1;
}
.plate.full::before {
  background: linear-gradient(100deg,
    rgba(42, 30, 20, 0.92) 0%,
    rgba(42, 30, 20, 0.82) 38%,
    rgba(42, 30, 20, 0.46) 72%,
    rgba(42, 30, 20, 0.68) 100%);
}
/* Weighted to the very top and the very bottom now that the wordmark and the
   index sit on the picture rather than above it — 10px letter-spaced caps need
   more help than a headline does. */
.plate.full::after {
  background: linear-gradient(to bottom,
    rgba(42, 30, 20, 0.86) 0%,
    rgba(42, 30, 20, 0.42) 16%,
    transparent 34%,
    transparent 62%,
    rgba(42, 30, 20, 0.52) 82%,
    rgba(42, 30, 20, 0.9) 100%);
}

.plate.full img, .plate.full video {
  /* ⚠️ `center 35%`, not `center`. These are all photographs OF A PERSON, and
     a portrait photograph cropped to a wide band with the middle kept shows a
     torso. Biasing upward is right far more often than it is wrong, so it is
     the default and a panel says otherwise with its own `focus`.

     ⚠️ It only matters on a wide window — a portrait picture in a portrait
     window is barely cropped at all, which is why every phone check of this
     page has passed while a desktop cut Fergus's head off. */
  object-position: center 35%;

  /* ⚠️ `multiply` is turned OFF here, and this is the whole reason the
     full-bleed version needed its own rule rather than a wider `.plate`.

     `mix-blend-mode` blends an element with its backdrop WITHIN ITS OWN
     STACKING CONTEXT — and the `position: fixed` above creates one. So the
     moment this went full bleed, `multiply` stopped seeing the cream page
     underneath and started blending against nothing. A photograph that had
     been a soft wash down the right-hand column came through at almost full
     strength and turned the entire page black, with the wordmark and the
     doors barely legible on top of it.

     It was RENDERED AND LOOKED AT, which is the only reason this is written
     down rather than shipped. The arithmetic said 0.26 would be comfortable.
     It was not remotely comfortable. */
  mix-blend-mode: normal;

  /* ⚠️ NOT grayscale here, and that is the colouring fix rather than a
     preference.

     Nick, 20 Sep 2026, on the first full-bleed render: *"just the colouring."*
     The column treatment desaturates the picture to grey, which is right when
     it is a sliver at the right-hand edge and wrong when it is the whole page:
     grey over cream goes muddy, and the page stops being cream at all. The
     warm ground is the design — it is the first thing named in `:root`.

     So the picture is toned INTO the paper instead of drained of colour.
     `sepia` pulls it towards the same warm brown as --paper and --ink;
     `brightness` lifts it off black, because the photograph is a near-black
     red-lit live shot and a dark picture at any opacity reads as dirt rather
     than as a photograph. */
  /* ⚠️ LIGHTENED 25 Sep 2026. Nick: *"make it brighter across the whole
     site."* Was `sepia(0.42) saturate(0.7) brightness(1.24) contrast(0.92)`.

     ⚠️ **The old values were tuned for a page that no longer exists.** The note
     below them argues the picture must be toned into the CREAM, because "grey
     over cream goes muddy and the page stops being cream at all". That was
     true when the pictures sat behind a cream panel. Since 22 Sep every panel
     is paper-on-photograph — `document.body.classList.add('photo-home')` runs
     unconditionally — so the ground behind a picture is dark, not cream, and
     42% sepia over a dark ground is just a muted picture.

     Some sepia stays, because the palette is warm and a cold photograph in the
     middle of it reads as a different site. It is half what it was, not none. */
  filter: sepia(0.2) saturate(0.98) brightness(1.18) contrast(0.98);

  /* How present the photograph is. ONE NUMBER on purpose — the same reasoning
     as TEMPO for the animation speeds: "make the photo stronger" has to stay a
     single edit or it stops being a thing anyone adjusts.

     ⚠️ Low, deliberately. The cream and the ink are the design; the photograph
     is behind them. Every value above about 0.25 starts taking the page's
     colour away rather than adding to it — which is exactly what Nick objected
     to. It is a look he picks, not a value that is correct. */
  /* Nick, 21 Sep 2026: *"make the opacity mask on the background vid/pics a bit
     less dense maybe 20% so pics are brighter"*. 0.14 -> 0.20.
 
     ⚠️ This is the ONE number, and it stays one number. The note above it has
     said since 20 Sep that anything above about 0.25 starts taking the page's
     colour away rather than adding to it — 0.20 is inside that, and it is a
     look Nick picks rather than a value that is correct. */
  opacity: var(--plate-strength, 0.20);
}

/* ⚠️ GONE. The caption labelled the photograph when it was a column down the
   right-hand side with its own space to sit in. Full bleed removed that space:
   `bottom: 0` became the window's bottom, so it landed in the index's row and
   smudged into "Follow/Contact", and being an auto-z sibling of a z-index: 1
   scrim it was painted under an almost-opaque wash on the way — an illegible
   ghost on top of the navigation.
   
   Deleted rather than re-anchored. It was labelling a picture nobody is asked
   to look at: the plate is aria-hidden with an empty alt precisely because it
   is a wash behind the words. */
.plate .caption { display: none; }

/* A panel whose photograph has not been supplied yet.
   NOT an empty rectangle: the design names what each one should be, so the
   placeholder says it. An unfilled slot that states its own brief reads as
   work in progress; a blank one reads as broken. Same call as the drawn
   release covers. */
/* No photograph yet. NOT a bordered box — this layer is behind the buttons
   now, and a rectangle with a border round it behind the words would look
   like a mistake rather than a slot. Just the brief, quietly, bottom right. */
.plate-empty {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; justify-content: flex-end; align-items: flex-end;
  padding: clamp(14px, 3%, 30px);
}
.plate-empty .hint {
  margin: 0; max-width: 20ch; text-align: right;
  font-family: var(--hand);
  font-size: clamp(14px, 1.4vw, 19px); line-height: 1.3;
  color: var(--pencil); opacity: 0.45;
}

/* ---- sub pages: written headings and pencil annotations -----------------
 *
 * Nick, 21 Sep 2026, choosing from the three explorations: "1a headings and
 * 1c annotations". So every panel below home opens the way home does — a word
 * written in the hand with a crayon line drawn under it — and the typeset
 * list underneath is then MARKED UP rather than redrawn: a ring round the row
 * that matters, a note in the margin in the accent colour, and the row rules
 * themselves drawn rather than ruled.
 *
 * The list stays typeset on purpose. A gig list or a discography is read by
 * scanning, and handwriting at six rows is slower to scan than type. The hand
 * does the heading, the edges and the pointing; the type does the facts.
 *
 * ⚠️ EVERY DRAWN LINE HERE IS GENERATED AT ITS MEASURED SIZE, by fitRules(),
 * for the same reason fitDoors() exists: a path authored at one width and
 * stretched to another with preserveAspectRatio="none" comes apart under the
 * crayon filter — the displacement is applied AFTER the scale, so a line
 * stretched 3x breaks into staggered dashes. These elements ship empty and
 * are filled once the browser knows how wide they are.
 */

.written { position: relative; display: inline-block; max-width: 100%; }
.written-word {
  display: inline-block;
  font-family: var(--hand);
  /* ⚠️ BROUGHT DOWN a quarter on 25 Sep 2026 — Nick: *"the page headings
     seem a bit big."* It was clamp(24px, min(4.6vw, 6.4vh), 48px), which
     put "Playlists I'm recommending" across half a desktop screen. The
     heading is written by hand and sits above a drawn rule; at 48px the
     rule became the widest thing on the page and the panel below it read
     as a footnote to its own title. */
  /* ⚠️ DOWN AGAIN ON 26 Sep 2026, and this is the SECOND reduction for the
     same reason — Nick: *"could the titles on each page be slightly smaller?
     Judge the best balance with the rest of the page."*

     36 was already a quarter off the original 48. What changed since is the
     page under it: the record shelf, the television and the tape deck are all
     large objects now, and a heading sized against a page of text is sized
     against the wrong thing. At 30 the heading names the panel and the object
     below it is plainly the subject.

     ⚠️ THE FLOOR MOVED TOO, 22 to 20. On a phone the heading is nearly always
     at the floor, so leaving it would have meant "slightly smaller" doing
     nothing at all on the screen Nick looks at most. */
  font-size: clamp(20px, min(2.9vw, 4.1vh), 30px);
  line-height: 1.05; color: var(--ink);
  /* ONE LINE. The heading is written, and the crayon line is drawn under the
     whole of it — a heading that wraps puts the underline through the middle
     of its own second line, and the write clip cuts that line off. */
  white-space: nowrap;
}
.written-ul {
  position: absolute; left: 0; bottom: -9px;
  width: calc(100% + 10px); height: 12px;
  color: var(--flame); overflow: visible; pointer-events: none;
}
.head-row {
  display: flex; align-items: flex-start; gap: clamp(12px, 2vw, 30px);
  flex-wrap: wrap;
  margin: 0 0 clamp(18px, 3.2vh, 34px);
}
.note {
  display: inline-block;
  font-family: var(--hand); line-height: 1.15; color: var(--flame);
  font-size: clamp(14px, min(1.45vw, 2.1vh), 19px);
}
/* ⚠️ Wide enough for the note to stay on ONE line: the write clip reveals a
   single line, and a note that wraps loses its second one. */
.head-row .note { padding-top: clamp(4px, 1vh, 12px); white-space: nowrap; }
.note-under { margin-top: clamp(6px, 1.2vh, 14px); }

/* The rules under the rows are drawn now, so the ruled ones go. ⚠️ Scoped to
   .pane to beat the ruled versions further down the sheet. */
.pane .rows { border-top: 0; }
.pane .rows li, .pane .row { position: relative; border-bottom: 0; }
.marked { position: relative; display: inline-block; }
.pane .row .where > .marked { display: inline-block; width: max-content; max-width: 100%; }
.pane .row > .note-inline { flex: 0 0 auto; align-self: center; }
.note-inline { margin-left: clamp(10px, 1.6vw, 22px); white-space: nowrap; }
.row-rule {
  position: absolute; left: 0; bottom: -4px;
  width: 100%; height: 9px;
  color: var(--ink); opacity: 0.45; overflow: visible; pointer-events: none;
}
.ring {
  position: absolute; left: -10px; top: -7px;
  width: calc(100% + 20px); height: calc(100% + 14px);
  color: var(--flame); overflow: visible; pointer-events: none;
}
.drawn-rule { display: block; }

/* ---- artwork -------------------------------------------------------------
   Nick, 21 Sep 2026: "want artwork for all releases included". A release with
   no artwork yet gets its initial in the hand rather than an empty square —
   same call as the panel placeholders. */
.rows.arted li { align-items: center; }
.art {
  flex: 0 0 auto; width: clamp(34px, 3.6vw, 48px); aspect-ratio: 1;
  overflow: hidden; display: grid; place-items: center;
  background: rgba(242, 227, 199, 0.12);
}
.art img, .cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.art-none { font-family: var(--hand); font-size: 15px; color: var(--ink); opacity: 0.65; }
/* ⚠️ `.withart` IS GONE, 24 Sep 2026, and its rules with it. It was the old EP
   layout — a small cover in a corner beside a tracklist — and the sleeve
   designs replaced it. Nothing in `app.js` has drawn a `.withart` since.

   ⚠️ **One of its leftovers was actively doing harm.** A phone override set
   `.cover { width: clamp(104px, 38vw, 170px) }`, which sat AFTER the sleeve's
   own rule and won — so the artwork was pinned at about 148px on a phone
   however large the new rule asked for. Nick asked twice for it to be bigger
   and nothing visible changed, because a dead layout was still holding it
   down. **Dead CSS is not inert; it is a rule that still applies.** */
/* The picture follows the frame down rather than overflowing it. */
/* ⚠️ THE EP TAB GETS A SMALLER CAP, because it is the only one with a tracklist
   under the sleeve. At 56vh the four tracks were pushed off the bottom of a
   panel that cannot grow. The deck tabs have a note and a button below the
   sleeve and nothing else, so they keep the larger one. */
/* ⚠️ MEASURED, NOT GUESSED. At 390x844 the pane is 528px, and the EP tab has
   to fit a heading, a tab row, a 153px tracklist, a note and the Odesli row
   under the sleeve. At the larger cap the tracklist was getting 65px of the
   153 it wanted — three rows visible and the third cut through.

   ⚠️ **The `vh` is of the VIEWPORT, not the panel**, and the panel is about
   two thirds of it once the header and the index are taken off. So these
   numbers look small and are not. */
/* ⚠️ NO PER-TAB CAP ANY MORE. There were two — one for the tab with a
   tracklist and one for the decks — and both were guesses at a ratio. The
   floor on the words does the same job without knowing which tab it is on. */
.cover-box {
  position: absolute; inset: 0; width: 100%; height: 100%;
  color: var(--ink); overflow: visible; pointer-events: none;
}
.door-cta { margin-top: clamp(10px, 2vh, 22px); margin-left: -2px; }

/* ---- the live carousel, along the foot of the releases panel -------------
   Nick, 21 Sep 2026: "make youtube carousel along bottom of releases that
   will show live performances of originals".

   Thumbnails, not more players. An embedded iframe costs a YouTube
   connection before anyone has asked to watch anything, and on a panel that
   already has a list above it four of them would not fit; the picture is
   swapped for the player on click. The strip scrolls sideways because the
   panel cannot grow downwards - the site does not scroll. */
/* ⚠️ THE STRIP IS PINNED TO THE FOOT AND THE LIST GIVES WAY.
   Six releases plus a row of thumbnails is taller than the pane, and the
   site does not scroll — so this panel becomes a column whose middle is
   allowed to shrink. The list scrolls inside itself if it has to; the
   heading and the strip always stay put. */
.panel-fit { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.panel-fit > .rows { flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-width: none; }
.panel-fit > .rows::-webkit-scrollbar { width: 0; }
.panel-fit > .head-row { flex: 0 0 auto; }
/* ⚠️ THE NEW PIECES HAVE TO BE PINNED TOO, or they stretch and squash the list.
   In a column flex the default is `flex: 0 1 auto` — which cannot grow but CAN
   shrink, so the player and the tab row were taking their share of the squeeze
   and giving the list about 80px: two rows visible and the third cut through
   the middle. Everything above `.rows` is fixed; only the list gives way. */
.panel-fit > .player,
.panel-fit > .playon-slot { flex: 0 0 auto; }
/* The record's pieces are pinned inside `.rec-card` — see that rule. */
/* ⚠️ NO FLOOR UNDER A RECORD'S WORDS, and this was a real fault rather than a
   refinement. `.sleeve-words` claims `min-height: 14rem` — 224px — because the
   block it was written for held three track ROWS and a note. A record's words
   are one line of note and one compact line of tracks, and with every note in
   the database still empty it was often one line of anything.

   It claimed the 224px regardless, and the way out — the Listen row — was
   pushed clean off the bottom of a panel that does not scroll. At 1400x900 it
   sat at y=911 in an 813px window. Measured, not guessed: nothing renders a
   scrollbar here, so there was no symptom to see except the row being gone.

   Scoped by `.rec ~`, because the Playlists panel still uses the block the
   floor was written for. */
/* ⚠️ A SMALL FLOOR, NOT NO FLOOR. At `min-height: 0` the block did the other
   wrong thing: on a wide window the flex row ran out of room and shrank it to
   ZERO, so the track line was inside a scroller with no height and clipped
   entirely. Measured at 1400x900 — `.sleeve-words h0` with a 23px child in it.
   4rem is a note line and a track line; below that it may still shrink and
   scroll, which is what the fade is for. */
/* ⚠️ THE WORDS STOP AT THE COVER'S EDGE. Nick, 25 Sep 2026: *"below the
   records in discography, where the tracks are listed and the notes show, limit
   that to the width of the album cover."* The band above them already did; the
   track line ran on past it, so the block under each record had two different
   right-hand edges. */
.rec-card .sleeve-words { min-height: min(4rem, 100%); width: var(--rec-sw); }
/* On the Latest release tab the flexible middle is the cover-and-tracklist
   block, so the live strip below it keeps its captions rather than being
   clipped off the bottom of the pane. */
/* ⚠️ `0 1 auto` and NOT `1 1 auto`. Growing made the cover-and-tracklist block
   fill the panel, which pushed the PLAY ON row down to the floor with half a
   screen of nothing between it and the last track — the two boxes read as
   belonging to the index along the bottom rather than to the record above
   them. `0` takes its content height; the `1` still lets it shrink and scroll
   on a short window, which is the part that matters. */
.panel-fit > .sleeve-body { flex: 0 1 auto; min-height: 0; }
/* The panel's own scroll is on `.rows`, so the player must not add one of its
   own by overflowing the column. */
.panel-fit > .player { margin-bottom: clamp(6px, 1.2vh, 12px); }

/* The foot of a panel: whatever it ends with sits on the bottom rule, with
   the signup on the left and the one door on the right of the same line.
   Nick, 21 Sep 2026. */
.panel-foot {
  margin-top: auto; flex: 0 0 auto;
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: clamp(12px, 2vw, 30px); flex-wrap: wrap;
  padding-top: clamp(8px, 1.6vh, 18px);
}
.panel-foot .ask { margin-top: 0; }
.panel-foot .door-cta { margin-top: 0; }
/* A card that has become its player. It keeps every measurement the button
   had — same width, same place in the scroll — so nothing shifts when it is
   pressed. Only the things that belonged to a button go. */
/* ---- the Spotify player -----------------------------------------------------
   The one thing on this page nobody drew. It cannot be restyled — Spotify
   serves its own frame — so it is given a width and left alone rather than
   fought with. Held to the body column's measure so it does not become the
   widest thing on the panel, which on a phone it otherwise would be. */
.player { margin-top: clamp(10px, 2vh, 20px); max-width: 420px; }
.player iframe {
  display: block; width: 100%; height: 152px; border: 0; border-radius: 12px;
}
/* ⚠️ THE TALL PLAYER, on the Latest release tab only. 352px is the height at
   which Spotify's embed lists the tracks and shows the artwork big — and that
   tab stopped writing its own tracklist on 23 Sep, so this player IS the
   tracklist now. Nick: *"we just keep spotify up to date and the page is up to
   date."*

   ⚠️ It has to cap against the pane, or it is 352px of a 614px panel that also
   has a heading, a tab row and the PLAY ON boxes — and on a short laptop the
   boxes go under the fold. `min()` lets it be 352 where there is room and give
   way where there is not. */
.player-tall iframe { height: min(352px, 46vh); }
.player-tall { max-width: 520px; }
@media (max-width: 720px) { .player { max-width: none; } }
/* ---- the index ----------------------------------------------------------- */

/* ⚠️ IT SITS IN THE HEADER NOW, above the rule rather than under the page.
   Nick, 25 Sep 2026: *"can we lose the bottom navigation and instead put it
   across the top on desktop and in a hamburger menu on mobile?"*

   ⚠️ The rule it used to carry on TOP is gone. The header already draws a line
   underneath itself, and two lines a few pixels apart read as a mistake. */
.index {
  position: relative; z-index: 1;      /* above the photograph — see .top */
  display: flex; flex-wrap: wrap; align-items: baseline; gap: clamp(20px, 4vw, 66px);
  padding: clamp(8px, 1.3vh, 14px) 0 0;
}
/* The whole index in the hand now, not just the page you are on — and the
   items are separated by a slash rather than by a rule under the current one.
   Written out, a row of names with slashes between them is what the index
   always was; the small caps and the border were type's way of saying it. */
/* ⚠️ THE INDEX IS THE HOME PAGE'S INDEX NOW, ON EVERY PAGE — 26 Sep 2026.
   Nick: *"Make the navigation and the rest of the site look like it does on
   the homepage."*

   It was handwriting at 15-19px with a slash between each pair. That reads as
   a hand-drawn list, which suited a page whose every heading was drawn — and
   it is a different navigation from the one on the front, which was the
   complaint.

   ⚠️ THE SLASHES GO. They were separating words that the gap already
   separates, and with lowercase italic between them they read as fractions.

   ⚠️ AND THE GAP WIDENS, which is doing the slashes' old job. Five words at
   26px apart with no separator run together; at 66px they are plainly a list.
   Removing the slash without opening the gap would have looked broken. */
.index a {
  display: flex; align-items: baseline; gap: 7px;
  font-family: var(--mark); font-style: italic;
  font-size: clamp(14px, 1.15vw, 17px);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: lowercase;
  padding-bottom: 0;
  color: var(--dim);
}
.index a:hover { color: var(--ink); }
.index a[aria-current="page"] { color: var(--ink); }
/* Nick, 21 Sep 2026: the numbers go. They were scaffolding that made a row of
   small caps read as an ordered index, and the labels do that on their own. */
.index .n { display: none; }

/* Where you are is carried by colour alone now — full strength against the
   others at --dim. With every item in the hand there is nothing left for an
   underline to distinguish, and a drawn rule under one of eight handwritten
   names read as a correction rather than as a marker. */
.index a[aria-current="page"] { border-bottom: 0; }

/* ---- type ---------------------------------------------------------------- */

.kicker {
  margin: 0 0 10px; font-size: 10px; font-weight: 600;
  letter-spacing: 0.24em; text-transform: uppercase; color: var(--dim);
}
.panel h2 {
  margin: 0 0 clamp(14px, 2.4vh, 26px);
  font-family: var(--display); font-weight: 700;
  font-size: clamp(25px, 3.8vw, 52px); line-height: 0.9;
  letter-spacing: -0.02em;
}
.panel h2 .hot { color: var(--flame); }
.lead {
  margin: 0 0 18px; max-width: 42ch;
  font-size: clamp(14px, 1.1vw, 17px); line-height: 1.6; color: var(--dim);
}
.hand { font-family: var(--hand); font-size: clamp(18px, 1.8vw, 24px); line-height: 1.2; margin: 0 0 6px; }

/* ---- buttons ------------------------------------------------------------- */

.btn, .btn-line {
  display: inline-block; padding: 12px 20px;
  font-size: 10px; font-weight: 600;
  letter-spacing: 0.2em; text-transform: uppercase; white-space: nowrap;
  cursor: pointer; font-family: var(--body);
}
/* ⚠️ The label on a SOLID button is named literally, for the same reason as
   .hard-fail: --ink and --paper are one colour now, so a solid button filled
   with --ink and lettered with --paper was cream on cream. The fill still
   comes from the token — it flips with the page — but the writing on top of it
   has to be the dark, always. */
.btn { background: var(--ink); color: #2A1E14; border: 1px solid var(--ink); }
.btn:hover { background: var(--flame); border-color: var(--flame); color: #F2E3C7; }
.btn-line { border: 1px solid var(--ink); background: transparent; color: var(--ink); }
.btn-line:hover { background: var(--ink); color: #2A1E14; }

/* ---- the home page's hand-annotated buttons ------------------------------ */

/* The doors: handwritten words with a box drawn round each.
   Nick, 19 Sep 2026 — "write first button left to right then draw the box
   around like it's hand drawn."

   `align-items: flex-start` so each is only as wide as its own words. Stretched
   to match, they would read as a menu bar rather than as six things someone
   wrote down. */
.doors {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: clamp(7px, 1.5vh, 16px); max-width: 560px;
}

/* ⚠️ THE HEIGHT HAS TO KNOW ABOUT THE WINDOW. Every previous attempt at making
   home fit a short window trimmed gaps and margins, and it kept not fitting,
   because the six door rows — the bulk of the panel — were a fixed 39px each
   whatever the window did: their type is vw, their padding's vh term never
   reached its floor. Six rows that cannot shrink are 234px of a 365px pane.
   
   `min(vw, vh)` on both: wide-but-short windows now take their size from the
   height, which is the dimension that is actually short. */
.door {
  position: relative;
  display: inline-block;
  padding: clamp(5px, min(1.1vh, 1.4vw), 13px) clamp(14px, 1.9vw, 24px);
  color: var(--ink);
}
.door-word {
  display: inline-block;
  font-family: var(--hand);
  font-size: clamp(17px, min(2.2vw, 3.3vh), 30px);
  line-height: 1.1;
  color: var(--ink);
  /* The reveal is a clip, not a fade, so the word appears letter by letter
     from the left — the same `write` the removed annotations used. */
}
/* The box is drawn round the outside of the words, so it must not add to the
   box the words sit in — hence absolute, and no pointer events of its own. */
.door-box {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  overflow: visible;
  pointer-events: none;
}
/* ⚠️ THE DOTS. A round cap on a dash of zero length still paints — so every
   box put a blob at its path's start point and sat there showing it until its
   own turn to draw came round, up to six seconds in. It read as a bullet
   beside each button, which is exactly what it was not.
   
   Butt caps on the boxes. The crayon filter is what makes these look drawn,
   not the cap shape, and the underlines under the signup keep their round
   ends where the length is never zero for long. */
/* ⚠️ `.edge` here too, and for the same reason as the photo-home rule above.
   The fill strokes are generated with ROUND caps on purpose — they overshoot
   the box top and bottom and are clipped, so the corners stay as solid as the
   middle, and a butt cap squares off exactly the overshoot that was doing
   that work. This rule was squaring all 48 of them. */
.door-box .edge, .drawn-box .edge { stroke-linecap: butt; }

.door:hover .door-word { color: var(--flame); }
.door:hover .door-box path { stroke: var(--flame); }

/* ---- Drums, covers etc --------------------------------------------------- */

/* One column on a narrow pane, two when there is room. The pane is already
   narrow by design, so this is about the pane rather than the window. */
/* ⚠️ Four 16:9 players stacked two-up is 174px more than the pane has, and no
   width rule fixes that — the grid asked how WIDE the pane was and never how
   tall. Capping each player's height is what makes the panel fit: the columns
   then follow from the height rather than the other way round. */

/* ---- the ask, under the doors -------------------------------------------- */

/* ⚠️ Tight on purpose. Six doors plus the signup is the whole home page, and
   the brief is that it fits without scrolling — on a 540px window the old
   rhythm put the email field and the button below the fold, which is the one
   conversion on the site. The vh terms do the work: roomy where there is room,
   and it closes up rather than overflowing where there is not. */
.ask { margin-top: clamp(6px, 2.2vh, 30px); max-width: 480px; }
.ask-kicker {
  margin: 0 0 clamp(3px, 0.7vh, 6px);
  font-size: 10px; font-weight: 600;
  letter-spacing: 0.24em; text-transform: uppercase; color: var(--dim);
}
.ask-words {
  margin: 0 0 clamp(5px, 1.4vh, 14px);
  font-family: var(--hand);
  font-size: clamp(13px, min(1.7vw, 2.6vh), 22px); line-height: 1.18; color: var(--ink);
}
.ask-words span { display: block; }

.ask-form { display: flex; flex-wrap: wrap; align-items: center; gap: clamp(5px, 1vh, 10px); }

/* The email field and its button, kept together on one line.
   Nick, 20 Sep 2026: "shorten the email entry box and put the button at the
   end of it so box and button are in line."

   A row of their own rather than two items in the form's flex — otherwise the
   button can wrap onto the next line on its own at an awkward width, which is
   exactly what "in line" rules out. `align-items: stretch` so the drawn box
   and the drawn button are the same height whatever the type does. */
.ask-row {
  display: flex; align-items: stretch; gap: 8px;
  flex: 0 1 auto; max-width: 100%;
}

/* The signup button. No background and no border of its own — the drawing is
   both, and the colour arrives as pencil strokes underneath the words. */
.ask-go {
  position: relative;
  /* ⚠️ `inline-block` IS LOad-BEARING, and it only became so on 23 Sep 2026.
     This button used to be a <button>, which browsers make inline-block by
     default. It is now an <a> when there is a signup link to send people to,
     and an <a> is plain `inline` — and an absolutely positioned child of an
     inline box has no height to be 100% OF. The drawing collapsed to nothing:
     no box, no white fill, just the word floating on its own.

     ⚠️ Every check passed while it was invisible. The markup was correct, the
     ~60 pencil strokes were all generated, the sequence was in the right
     order, and the stylesheet said white. Caught by screenshotting the live
     build and looking at it. `.door` has carried this same line since the day
     the doors were built, for the same reason. */
  display: inline-block;
  flex: 0 0 auto;
  padding: clamp(11px, 1.4vh, 15px) clamp(18px, 2vw, 26px);
  background: transparent; border: 0; cursor: pointer;
  font-family: var(--hand); font-size: clamp(15px, min(1.5vw, 2.4vh), 19px);
  color: var(--ink);
}
.ask-go .go-word { position: relative; z-index: 1; display: inline-block; }
.ask-go .drawn-box { z-index: 0; }

/* ---- the signup button is WHITE, filled, with a dark word ------------------
   Nick, 23 Sep 2026: *"Make that box coloured in with the white colour, and
   make the 'Sign up' word dark so that it can be seen."*

   ⚠️ THIS EXACT COMBINATION WAS TRIED ONCE AND FAILED, and the note that
   recorded the failure is still a few hundred lines up under `.photo-home`:
   *"cream strokes and a dark word... rendered as a pale block with the word
   lost among the gaps between strokes."* It is worth knowing WHY it failed,
   because the fix is not the colour — it is the gaps.

   The pencil colouring is deliberately rough: two passes of separate strokes
   with the ground showing between them. When the strokes were DARK that
   roughness was the character of the thing, because every gap showed cream and
   cream is close to the paper it sits on. Turn the strokes pale and the gaps
   show the PHOTOGRAPH instead — a white block shot through with dark slots,
   and a dark word laid over it disappears into them.

   So the three rules below are not three preferences, they are one fix: the
   strokes are explicitly white, they are fully opaque, and nothing is allowed
   to thin them. The roughness survives at the EDGES of the block, which is
   where it reads as drawn anyway, rather than through the middle where the
   word has to be legible.

   ⚠️ Hard-coded hex, not `--ink` or `--paper`, and on purpose: this button is
   the one thing on the page that brings its own ground with it, so it must NOT
   follow the home page's flip. That was already true when it was dark; it is
   true in the other direction now. */
.ask-go .drawn-box .in { stroke: #FFFFFF; stroke-opacity: 1; }
/* The drawn outline stays dark, so the hand still shows round a white block —
   without it, white on cream is a shape with no edge. */
.ask-go .drawn-box .edge { stroke: #33200F; }
/* Dark at rest as well as at the end of the animation, so the button is
   readable in the seconds before it is coloured and if the animation never
   runs at all. */
.ask-go .go-word { color: #33200F; }

/* ⚠️ Hover touches the OUTLINE ONLY. It used to be `path`, which is both the
   outline and every one of the ~60 fill strokes — fine while they were dark
   ink going a slightly different dark, and a flashing orange block now that
   they are white. */
.ask-go:hover .drawn-box .edge { stroke: var(--flame); }
.ask-go:hover .drawn-box line { stroke: var(--flame); }

/* ⚠️ `.drawn-field` IS GONE, 25 Sep 2026, with the email box it drew a line
   round. Nick: *"remove the signup box and instead have a link to this page to
   sign up (use the button for this)."* The button is the whole control now.
   `.drawn-box` stays — it is what draws the button itself, and the doors. */
.drawn-box { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }

.ask-small {
  flex-basis: 100%;
  font-family: var(--hand);
  font-size: clamp(12px, min(1.2vw, 2.1vh), 16px); line-height: 1.3; color: var(--dim);
}

/* Same reasoning as the doors: the box starts invisible, so anyone who reaches
   the button before it is drawn still gets an outline. */
.ask-go:focus-visible .go-word { animation: none !important; clip-path: none !important; }
.ask-go:focus-visible .drawn-box path {
  animation: none !important; stroke-dashoffset: 0 !important;
}
/* Focused before it is coloured: leave it uncoloured and keep the words dark,
   rather than showing pale words on cream.
   ⚠️ The hex, not `--ink` — on a photo home page `--ink` is the PAPER colour,
   so this rule used to force the word pale on exactly the pages where it was
   hardest to read. */
.ask-go:focus-visible .go-word { color: #33200F !important; }

/* ⚠️ A door whose animation has not started yet must still be REACHABLE.
   `write` is a clip-path and `draw-box` starts at a full dashoffset, so for the
   first few seconds a door is invisible — but it is a link, and a keyboard or
   a screen reader can reach it before it appears. This keeps the words present
   for anyone not looking at the screen, and the focus ring works throughout. */
.door:focus-visible .door-word { animation: none !important; clip-path: none !important; }
.door:focus-visible .door-box path { animation: none !important; stroke-dashoffset: 0 !important; }
/* The animation itself is INLINE on the path and on each line, exactly as the
   design has it — see the note above DOORS in app.js. The stylesheet must not
   also declare it, or the two can drift apart. */
.note > span {
  font-family: var(--hand);
  line-height: 1.32; color: var(--pencil);
}
.note .say-big   { font-size: clamp(14px, 1.2vw, 17px); line-height: 1.3; }
.note .say-small { font-size: clamp(13px, 1.1vw, 15px); line-height: 1.35; }
.note span span { display: block; }

/* ---- lists --------------------------------------------------------------- */

.rows { margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--ink); }
.rows li, .row {
  display: flex; align-items: baseline; gap: 14px;
  padding: 10px 0; border-bottom: 1px solid var(--faint);
}
.rows .n, .row .n {
  width: 22px; flex: 0 0 auto;
  font-size: 10px; font-weight: 600; letter-spacing: 0.18em; color: var(--dim);
}
.rows .title, .row .title {
  flex: 1; min-width: 0;
  font-family: var(--display); font-weight: 500;
  font-size: clamp(16px, 1.4vw, 21px); line-height: 1.2;
}
.rows .title a:hover, .row .title a:hover { color: var(--deep); }
.rows .meta, .row .meta { font-size: 11px; letter-spacing: 0.1em; color: var(--dim); }
/* ⚠️ A ROW'S SECOND LINE — 28 Sep 2026, for the note on an unreleased demo.
   `.title` is a flex child with `min-width: 0`, so the note has to be a BLOCK
   inside it rather than another child of the row: as a sibling it joined the
   flex line and pushed the running time off the end. */
.rows .title .sub {
  display: block; margin-top: 2px;
  font-family: var(--body); font-weight: 400;
  font-size: clamp(12px, 1vw, 13px); line-height: 1.4; color: var(--dim);
}
/* A quieter lead, for a second sentence that is an aside rather than the ask. */
.lead.dim { color: var(--dim); font-size: clamp(13px, 1.05vw, 14px); }
/* Sitting under the sign-up button rather than above it, so it needs its own
   space away from the form it follows. */
.lead.dim.after-form { margin-top: 18px; }
.row .when {
  width: 84px; flex: 0 0 auto;
  font-size: 10px; font-weight: 600; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--dim);
}
.row .where { flex: 1; min-width: 0; }
.row .where b { display: block; font-family: var(--display); font-weight: 500; font-size: clamp(15px, 1.3vw, 19px); line-height: 1.15; }
.row .where span { display: block; margin-top: 2px; font-size: 12px; color: var(--dim); }
.row .tickets {
  flex: 0 0 auto; font-size: 10px; font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase;
  border-bottom: 1px solid var(--faint);
}
.row .tickets:hover { color: var(--deep); border-color: var(--ink); }

/* ---- forms --------------------------------------------------------------- */

/* ⚠️ THE SIGNUP IS A LINK NOW, not a form. `.form` still belongs to the
   booking enquiry, which is a real form and posts somewhere. */
.signup {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
  margin: 10px 0 0; max-width: 460px;
}
.signup .lead { margin: 0; font-size: 12px; flex: 1 1 180px; }
/* An `<a>` wearing a button's clothes: it needs the padding a button gets for
   free, and a line-height, or it sits a couple of pixels high beside the text. */
a.btn { display: inline-block; padding: 9px 18px; line-height: 1.25; text-decoration: none; }

.form { display: flex; flex-wrap: wrap; gap: 8px; max-width: 460px; }
.form input, .form textarea {
  flex: 1; min-width: 190px; padding: 12px 14px;
  background: transparent; border: 1px solid var(--ink); color: var(--ink);
  font-family: var(--body); font-size: 13px; outline: none;
}
.form input:focus, .form textarea:focus { background: var(--wash); }
.form textarea { min-height: 96px; resize: vertical; flex-basis: 100%; }
.form .wide { flex-basis: 100%; }
.form-msg { margin: 10px 0 0; font-family: var(--hand); font-size: 17px; line-height: 1.4; }
.form-msg.bad { color: var(--deep); }

/* Off-screen, not display:none — a bot fills what it cannot see, and
   display:none is the first thing they learn to skip. */
.honey {
  position: absolute; left: -9999px; width: 1px; height: 1px;
  overflow: hidden;
}

/* ---- odds ---------------------------------------------------------------- */

/* ⚠️ LITERAL COLOURS, NOT THE TOKENS, and that is the cost of moving the flip
   from .pane to the body. --ink and --paper are now the SAME VALUE document
   wide, so `background: var(--ink); color: var(--paper)` painted cream on
   cream and this panel — the only "can't load" message there is — came out
   blank. Anything outside .pane that pairs the two tokens has to name its
   colours; there is no longer a contrast between them to rely on. */
.hard-fail {
  position: fixed; inset: auto 0 0 0;
  padding: 20px clamp(16px, 2.6vw, 34px);
  background: #33200F; color: #F2E3C7;
}
.hard-fail a { color: #F2E3C7; text-decoration: underline; }
.stale {
  position: fixed; left: 0; right: 0; top: 0; margin: 0;
  padding: 7px 10px; text-align: center;
  background: var(--flame); color: var(--paper);
  font-size: 10px; font-weight: 600; letter-spacing: 0.18em; text-transform: uppercase;
}

/* ---- the phone ----------------------------------------------------------- */

/* Below this width the two columns cannot both be read, so the photograph
   goes above the words rather than beside them, and the page is allowed to
   scroll. A fixed full-height layout on a phone fights the browser chrome. */
@media (max-width: 760px) {
  /* ⚠️ THE PHONE USES THE SAME FRAME AS EVERYTHING ELSE, from 21 Sep 2026.
     Nick: *"these all need to have the header and footer fixed on phone and
     content in the centre part scrolls vertically"*.

     This block used to say `html, body { overflow: auto; height: auto }` and
     `.shell { position: static; min-height: 100dvh }` — the whole page
     scrolled, header and index included.

     ⚠️ **That was not a phone compromise. It was built for a layout that no
     longer exists.** Nick, 19 Sep: *"I want the photo underneath on phone"* —
     the picture left the layer behind the words and became a 4:3 panel stacked
     BELOW them, which needs the page to scroll or the picture has nowhere to
     go. On 21 Sep he reversed that for every panel, and `render()` now sets
     `plate full` unconditionally, which is `position: absolute`. So nothing
     has been stacked below the words since — **the page went on scrolling to
     make room for a picture that had stopped being in the flow.**

     The rules further down that put `.plate` in the flow at 4:3 are dead for
     the same reason. They are left rather than deleted because the decision
     they serve was reversed once and could be again; if it is, this comment is
     where to start. */
  html, body { overflow: hidden; height: 100%; }

  /* ---- the menu ---------------------------------------------------------
     ⚠️ ON A PHONE THE INDEX IS A DROP-DOWN, not a row. Nick, 25 Sep 2026:
     *"...and in a hamburger menu on mobile"*. Eight handwritten names wrapped
     to three lines and ate a third of a 844px-tall screen; the panel below is
     what he is there to read.

     ⚠️ It is `display: none` until opened, not `height: 0` or `opacity: 0`. A
     menu that is merely invisible is still in the tab order, so a phone
     keyboard walks through eight hidden links before reaching the page.

     ⚠️ `top: 100%` is measured against `.top`, which is inside the shell's
     side padding — so the sheet lines up with the wordmark rather than with
     the window. That is deliberate: it reads as part of the header. */
  /* ⚠️ THE BURGER GOES BACK TO THE TOP-RIGHT CORNER, 26 Sep 2026. When the
     header became one row, `.top-row` became a COLUMN — name above tagline —
     and the burger, which lives inside it, went to the bottom of that column
     and sat under the tagline on the left. It looked like a third line of the
     header rather than a control.

     ⚠️ IT IS PULLED OUT OF THE COLUMN RATHER THAN MOVED IN THE MARKUP,
     because the markup order is what a screen reader follows: name, tagline,
     then the control that opens the menu. Absolute positioning changes where
     it is drawn and not where it is read. */
  .top { position: relative; }
  .top-row { padding-right: 46px; }
  .burger {
    display: flex;
    position: absolute; top: 0; right: 0;
  }
  .index {
    position: absolute; top: 100%; left: 0; right: 0;
    display: none;
    flex-direction: column; align-items: flex-start; gap: 0;
    margin: 0; padding: 4px 0 12px;
    background: var(--menu-bg);
    border-bottom: 1px solid var(--faint);
    box-shadow: 0 18px 30px -12px rgba(0, 0, 0, 0.55);
  }
  .index.open { display: flex; }
  /* The slashes are what separated names on ONE line. Stacked, each name has
     a line of its own and a slash in front of it is just a stray mark. */
  /* ⚠️ The slash override is gone with the slashes themselves — a rule that
     turns off something that no longer exists reads as a feature. */
  .index a {
    width: 100%; font-size: 19px; padding: 10px 0;
  }
  /* A whole row is the target, not the six characters of the word. */
  .index a[aria-current="page"] { color: var(--ink); }
  /* On a phone the picture goes UNDERNEATH the words, not behind them.
     Nick, 19 Sep 2026: "I want the photo underneath on phone."

     So it comes back INTO the flow and the column stacks: words, then
     picture. Behind-the-words works on a wide screen because there is space
     to the right that the text never reaches; on a phone there is no such
     space, so a wash behind the buttons is just a photograph with writing
     on it. */
  .body { display: flex; flex-direction: column; gap: 18px; min-height: 0; }
  /* The middle is the only thing that moves. `min-height: 0` on both, or a
     flex item refuses to shrink below its content and the scroll never
     starts — it just pushes the index off the bottom of the screen. */
  .pane { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
  .plate {
    position: relative;
    top: auto; bottom: auto; right: auto;
    width: 100%;
    aspect-ratio: 4 / 3;
    /* Still softened at the top, so it joins the cream above it rather than
       starting with a hard edge. */
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.6) 14%, #000 34%);
    mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.6) 14%, #000 34%);
  }
  .plate img { opacity: 0.8; }   /* underneath, not under type — it can be stronger */

  /* ⚠️ HOME IS THE EXCEPTION, AND IT REVERSES A DECISION.
     
     Nick, 19 Sep 2026: *"I want the photo underneath on phone"* — which is the
     block above: the picture leaves the layer behind the words and becomes a
     4:3 panel stacked below them.
     
     Nick, 21 Sep 2026: *"photo/video to completely fill the screen on phone (if
     I draw the window to phone width the bottom quarter of the screen is the
     beige background colour)."* That is this. The earlier instruction still
     holds for every OTHER panel, and the reason it does is that those panels
     are text — a wash behind a tracklist on a narrow screen is a photograph
     with writing on it. Home is six buttons, and now pale ones. */
  .plate.full {
    position: absolute; inset: 0;
    width: auto; aspect-ratio: auto;
    -webkit-mask-image: none; mask-image: none;
  }
  /* The band has to be tall enough to be worth filling. Without this the words
     set the height and the picture is a strip behind them — which is the
     'bottom quarter is beige' Nick reported, in a different disguise. */
  body.photo-home .body { min-height: 68dvh; }
  .plate-empty { position: absolute; }
  .pane { justify-content: flex-start; }

  /* ⚠️ SUPERSEDED the same day by `.pane { flex: 1 1 auto }` above, which does
     this for every panel rather than only the ones with a foot. Kept because
     the reasoning is the reasoning for both, and because it names the symptom
     somebody will search for.

     THE PANE HAS TO GROW FOR A PANEL WITH A FOOT.
  
     Nick, 21 Sep 2026, on the Live page: *"move 'get notified about new dates'
     to the bottom, just above the line"*.
     
     `.panel-foot` is pushed down by `margin-top: auto`, which only does
     anything if the panel has spare height to push into. `.panel-fit` takes
     its height from `.pane` — and the rule a few lines above sets
     `.pane { flex: 0 0 auto }` on a phone, so the pane is exactly as tall as
     its words and there is no spare height at all. The signup therefore sat
     directly under the last gig row with the rest of the screen empty below
     it, while on a desktop it sat correctly on the bottom rule. **Same markup,
     same CSS, opposite result — which is why this was found by looking at a
     phone-width render and could not have been found by reading either file.**
     
     `.pane { flex: 0 0 auto }` was written when the picture was IN the flow
     below the words, as a 4:3 panel, and had to not be squashed. Every plate
     is `position: absolute` now (`.plate.full`, and `.plate-empty` above), so
     nothing is below the pane to protect any more — but it is scoped to panes
     that actually contain a fit panel rather than lifted wholesale, because
     the rule above is load-bearing for a layout that could come back.
     
     A browser without `:has()` ignores this and gets the old behaviour, which
     is wrong but not broken. */
  /* ⚠️ THE TRACKING GOES ON THE PHONE TOO. This override outlived the rule it
     was overriding — it still said 0.18em after the desktop rule had dropped
     to 0, so the tagline stayed letterspaced on exactly the screen nobody
     checks. Found by the suite, not by looking. */
  .standfirst { font-size: 10px; }
}


/* ---- "New live date" — the orange flag beside a door ----------------------
   Nick, 23 Sep 2026: *"When there is a new release on a new live date or a new
   release, we need that orange writing and an arrow pointing at the thing."*

   ⚠️ THE ROW EXISTS ONLY WHEN THERE IS A FLAG. See the note in `door()` — the
   flag has to sit outside the anchor or fitDoors() redraws the hand-drawn box
   round the door AND the flag. `.doors` is a column with `align-items:
   flex-start`, so a row and a bare anchor sit identically; nothing moves on the
   five doors that have no flag. */
.door-row { display: flex; align-items: center; gap: 2px; max-width: 100%; }

/* ⚠️ `--flame` and not a hex, so the flag stays orange through the home page's
   flip to pale-on-photograph. Orange is the site's ONE accent colour and this
   is the one place it is used to mean "look here" rather than "this is a
   link" — which is why nothing else on the home page is allowed to be orange.
   Two orange things and neither of them is a flag any more. */
.flash {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--flame);
  /* It must never push a door off the side of a narrow phone. The words wrap
     before the row does. */
  min-width: 0;
}
.flash-arrow {
  flex: 0 0 auto;
  width: clamp(26px, 3.4vw, 44px); height: auto;
  overflow: visible;
}
.flash-word {
  font-family: var(--hand);
  font-size: clamp(12px, min(1.4vw, 2.1vh), 17px);
  line-height: 1.15;
  /* Smaller than the door it points at, on purpose: it is an annotation in the
     margin, not a second button competing with the one it is pointing to. */
}

/* ⚠️ ON A PHONE IT STAYS BESIDE THE DOOR, and the words wrap instead.

   The first version dropped the flag under the door and turned the arrow a
   quarter turn to keep it pointing. That was wrong twice over: a rotated
   element keeps its ORIGINAL layout box, so a 44x22 arrow rotated upright
   still reserves 44px across and 22px down and the drawing spills out of it —
   and it was solving a problem the numbers say does not exist. The longest
   door is about 140px of a 390px phone, and "New live date!" at this size is
   about 90px. It fits beside it with room to spare.

   So the phone only makes the arrow smaller and lets the words take two lines
   if a longer flag ever needs them. `min-width: 0` on `.flash` is what allows
   that — without it a flex child refuses to shrink below its content and
   pushes the door off the screen instead of wrapping. */
@media (max-width: 560px) {
  .flash-arrow { width: 22px; }
  .flash-word { overflow-wrap: anywhere; }
}

/* Same reasoning as the doors and the signup: a thing whose animation has not
   started yet must still be READABLE if the animation never runs. */
@media (prefers-reduced-motion: reduce) {
  .flash-word { clip-path: none !important; }
  .flash-arrow path { stroke-dashoffset: 0 !important; }
}

/* ---- the two tabs on the Releases panel -----------------------------------
   ⚠️ THE TABS ARE GONE, 25 Sep 2026. Nick: *"remove the 'latest release' /
   'all releases' tagging. Call this Discography."* Their rules are deleted
   rather than left unused — a leftover `.withart` phone rule silently defeated
   two attempts at sizing the artwork on 24 Sep, so dead CSS is not inert here.

   ⚠️ WHAT THEY GOT RIGHT IS WORTH KEEPING IN MIND: they were plain links to two
   real addresses, so they cost no state and no handler, the back button worked
   and a tab could be bookmarked. Both addresses still exist and both still land
   on the shelf. */
/* The same drawn underline the headings use, and the same orange. This is the
   site's one accent colour doing the one job it does everywhere else: saying
   where you are, not that something is a link. */
/* ---- PLAY ON: [Spotify] [All] ---------------------------------------------
   Nick's own shorthand, 23 Sep 2026 — square brackets meaning a box. The boxes
   are `doorLink`s, so they are drawn by the same hand as every other button on
   the site rather than being a new kind of thing. */
.playon {
  display: flex; align-items: center;
  gap: clamp(6px, 1vw, 12px);
  flex-wrap: wrap;
  margin-top: clamp(8px, 1.6vh, 18px);
}
.playon-lbl {
  font-family: var(--body);
  font-size: 11px; font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--dim);
  white-space: nowrap;
}
/* ⚠️ `doorLink` carries `.door-cta`, which has a top margin of its own for the
   places it sits under a list. In a row it would push the boxes below the
   label. */
.playon .door-cta { margin-top: 0; }

/* ---- a release row that loads itself into the player ----------------------
   ⚠️ Only rows that HAVE a Spotify address are pressable — `li.playable` is
   added when one is found, so a release with an empty cell is an ordinary row
   rather than a button that does nothing. */
li.playable { cursor: pointer; }
li.playable:hover .title { color: var(--flame); }
/* The row currently in the player. Marked rather than merely hovered, because
   after pressing one the player is the only other thing that says which record
   is loaded, and it is above the fold on a phone only some of the time. */
li.playable[aria-current="true"] .title { color: var(--flame); }
li.playable[aria-current="true"] .n { color: var(--flame); }

/* ---- the booking form, where it sits under the socials --------------------
   Nick, 23 Sep 2026: *"add a contact form to the follow page."*

   ⚠️ THE GAP IS THE WHOLE RULE HERE. The socials list draws a hand-drawn rule
   under its last row, and that rule overshoots the row's width by design — so
   with the default spacing it ran straight through the first line of the
   invitation underneath it. Rendered and read; nothing measures a collision. */
.ask-line {
  margin-top: clamp(16px, 3vh, 34px);
  padding-top: clamp(4px, 0.8vh, 10px);
}
/* The form is the second thing on this panel, not the first — a promoter is
   the rarer visitor, and the socials are a glance. */
.ask-line + .form { margin-top: clamp(8px, 1.4vh, 16px); }

/* ---- sleeves: the EP, and the two decks -----------------------------------
   Chosen from `mockup/2026-09-24-sleeves.html` on 24 Sep 2026 — option 2 for
   the EP, arrows under the sleeve for the decks. */

/* On a phone the sleeve is the whole width and the words follow it. On a
   laptop they sit side by side, because a 300px square with half a screen of
   empty paper beside it is worse than either. */
.sleeve-body { display: flex; flex-direction: column; gap: clamp(10px, 2vh, 20px); min-height: 0; }
/* ⚠️ THE LAPTOP SLEEVE BLOCK IS GONE, 25 Sep 2026, with the square and the
   band before it. It put the artwork beside the words on a wide screen. There
   is nothing beside the words any more — the artwork is behind them, at every
   width, which is the one thing all three failed layouts were trying to
   arrange and could not. */

/* ⚠️ A SLEEVE HAS A LARGEST SENSIBLE SIZE. Left to fill a laptop it became a
   square big enough to push the note and the tracklist off the bottom of a
   panel that cannot grow. A record sleeve is about the size of a record
   sleeve. */

/* ⚠️ THE FLOOR, and it is the whole mechanism. Nick asked for a layout that
   *"shows at least the first three songs, allows a scroll, makes it obvious
   that there's a scroll, and making room for the cover note."*
 
   `19rem` is three track rows, a two-line note and the breathing room between
   them. It is in `rem` and not `vh` on purpose: it is a claim about how much
   TEXT has to be visible, which does not change with the size of the screen.
 
   The sleeve above it shrinks to whatever is left, so a short phone gets a
   smaller record sleeve rather than a hidden tracklist. */
.sleeve-words {
  /* ⚠️ `0 1 auto` — it claims its floor and does NOT grow. At `1 1 auto` it
     took the whole panel and squashed the sleeve to nothing, which is the
     opposite fault to the one being fixed. */
  flex: 0 1 auto;
  /* Three track rows and a two-line note — about 12rem. Measured, not guessed:
     four rows are 149px, so a row is ~37px and three are ~112, and a note runs
     to about 60. In `rem` rather than `vh` because it is a claim about how much
     TEXT must be visible, which does not change with the size of the screen. */
  /* ⚠️ 14rem, not 12. At 12 a record WITH a cover note showed two tracks and
     half of a third — the note eats the top of the block, and Nick asked for
     three songs AND room for the note, not one or the other. Three rows are
     about 120px, a two-line note about 75, and the hint below about 18. */
  min-height: min(14rem, 100%);
  overflow-y: auto; scrollbar-width: none;
}
.sleeve-words::-webkit-scrollbar { width: 0; }

/* The note, in the hand — liner notes rather than an essay. */
.cover-note {
  font-family: var(--hand);
  font-size: clamp(14px, min(1.5vw, 2.2vh), 18px);
  line-height: 1.35;
  margin: 0 0 clamp(8px, 1.6vh, 16px);
  max-width: 46ch;
}

.kind {
  font-family: var(--body); font-size: 10px; font-weight: 600;
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--flame);
  margin: 0 0 3px;
}

/* ⚠️ `.tracks-press` IS GONE, 25 Sep 2026, with the pressable tracklist it
   styled. Every track row used to be a full-width link out to that song, and
   the rule above explained at length why the whole row was the target rather
   than the title.

   It was deleted rather than left unused. Nick's instruction on 25 Sep:
   *"in the EP the songs listed shouldn't click out - only the widget enables
   click out."* A tracklist on a record sleeve is printed matter, not a menu,
   and that is the point of the treatment that replaced it — see `.rec-tracks`
   below. The reasoning for the old one is kept here because it is still true
   of any list that IS pressable, and it was learned by rendering rather than
   by testing. */

/* ===========================================================================
   A RELEASE: THE RECORD COMING OUT OF ITS SLEEVE
   ---------------------------------------------------------------------------
   The layout Nick chose on 25 Sep 2026 — `mockup/2026-09-25d-m-refined.html`.
   `app.js` carries the geometry and the reasoning; this is what draws it.

   ⚠️ EVERY POSITION IS A PERCENTAGE OF THE PANEL'S WIDTH, handed in from
   `recordGeom()` as custom properties. That is what lets one set of rules serve
   a phone and a desk with no breakpoint at all — and it is also why none of
   these numbers should be typed in here. Change `RECORD_*` in app.js.
   ========================================================================= */
/* ⚠️ CAPPED BY HEIGHT, NOT BY WIDTH, and this is the rule that makes one
   layout serve a phone and a desk.

   Everything inside is a share of the card's width, so on a phone `100%` is
   exactly right. On a wide panel it is not: at 1330px the sleeve came out
   778px square and pushed the band, the track line and the way out off the
   bottom of a panel that does not scroll.

   `--rec-inv` is 100 / RECORD_SW, handed in from `recordGeom()`. It is NOT
   typed here: the moment the sleeve's share changes in app.js, a constant in
   this file would be silently wrong and nothing would say so.

   ⚠️ TWO LIMITS, AND THE SECOND IS THE ONE THAT WAS MISSING. 40vh is the share
   of the screen the record may have. But the heading, tabs, band, words and
   way out cost a FIXED number of pixels that does not shrink with the window,
   so on a short screen a proportional record plus fixed furniture overflows and
   the way out goes off the bottom — measured at 500x668, the Listen row sat
   12px past the floor. `--rec-furniture` is that cost, measured: heading and
   tabs ~186, band 38, words 64, way out 52, gaps ~40. */
.rec-card {
  width: 100%;
  --rec-furniture: 380px;
  --rec-tall: min(40vh, calc(100vh - var(--rec-furniture)));
  display: flex; flex-direction: column; min-height: 0; flex: 0 1 auto;
  width: min(100%, calc(var(--rec-tall) * var(--rec-inv, 1.71)));
}
/* ---- the shelf ------------------------------------------------------------
   Nick, 25 Sep 2026: *"Call this Discography and just have them all as one
   horizontal scroll."*

   ⚠️ A REAL SCROLLER, not arrows that re-render a card. A phone swipes it, a
   trackpad swipes it, and every record is reachable from a keyboard because
   each card holds real links. Snapping is what makes it read as a shelf of
   records rather than a smear — without it a swipe leaves you between two.

   ⚠️ THE SCROLLBAR IS HIDDEN, which is why the dots underneath are not
   decoration: with no bar and no arrows, a scroller never says how many there
   are or where you are. The dots are the only thing that does. */
.disco {
  /* ⚠️ `stretch`, so every card is the same height and the Listen boxes line
     up across the shelf. At `flex-start` they sat at whatever height each
     card's own content ended — a record with a track line pushed its box
     lower than one without, and a row of boxes at four different heights
     reads as a mistake rather than a shelf.

     Safe now that `fitRecordCard()` measures the overrun and corrects: an
     earlier attempt at stretch clipped the Listen row, because the sizing was
     adding up the parts instead of asking how far the content overran. */
  display: flex; align-items: stretch;
  gap: clamp(16px, 3vw, 40px);
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  /* ⚠️ `1 1 auto`, so the shelf TAKES the room left in the panel rather than
     taking its height from its cards. That is what makes `fitRecordCard()`
     converge: the height it is fitting into stops moving while it works. With
     `0 1 auto` the shelf shrank as the cards shrank and the measurement chased
     its own tail. */
  flex: 1 1 auto; min-height: 0;
  /* Room for the shadow the sleeve throws, which a clipped scroller would cut. */
  padding-bottom: 4px;
}
.disco::-webkit-scrollbar { height: 0; width: 0; }
.disco:focus-visible { outline: 2px solid var(--flame); outline-offset: 4px; }
.disco-card {
  flex: 0 0 auto;
  display: flex;
  scroll-snap-align: start;
  /* The width follows from how tall the record may be, so a desk shows two or
     three side by side — which is what makes it read as a discography. */
  width: min(100%, calc(var(--rec-tall, 260px) * var(--rec-inv, 1.71)));
}
/* ⚠️ ON A PHONE THE WIDTH LEADS INSTEAD, and this is a real fault being fixed
   rather than a preference. Nick, on his own phone: *"nearly all of the next
   record in line is showing — it's nice to see a little bit of it but we are
   leaving a lot of real estate on the table."*

   Two whole records were fitting across a phone, because the card's width was
   derived from a record height that the vertical fit had squeezed almost to its
   floor. The screen was two-thirds empty below the button and the artwork was
   half the size it could be.

   So below 760px the card takes 86% of the shelf — one record, and a sliver of
   the next to say there are more. The record's height then follows from the
   width, which is the right way round when width is the scarce thing. */
@media (max-width: 760px) {
  .disco-card { width: 86%; }
}
/* ⚠️ The dots are BUTTONS, kept from the deck they replace and for its
   reasons: reachable from a keyboard, announced to a screen reader. */
.disco-dots {
  display: flex; justify-content: center; gap: 9px; flex: 0 0 auto;
  margin: clamp(10px, 1.8vh, 18px) 0 0;
}
.disco-dots .dot {
  width: 7px; height: 7px; border-radius: 50%; padding: 0;
  border: 0; background: var(--faint); cursor: pointer;
  transition: background 0.2s ease;
}
.disco-dots .dot.on { background: var(--flame); }
.disco-dots .dot:hover { background: var(--dim); }

/* ---- the Videos panel's two subsections ----------------------------------
   Nick, 25 Sep 2026: *"call that section 'Videos' with subsections: Live
   Originals, Covers/Drums etc."* ⚠️ An empty subsection draws NOTHING — not a
   heading with a gap under it, which reads as something failing to load. */

.panel-fit > .rec-card { flex: 0 1 auto; min-height: 0; }
/* ⚠️ The deck's arrows cost about 44px, and the record has to give it up.
   Without this they were drawn straight across the Listen widget — the column
   simply ran past the floor of a panel that does not scroll. */
.rec-card.has-deck {
  --rec-furniture: 436px;
  --rec-tall: min(34vh, calc(100vh - var(--rec-furniture)));
}
/* ⚠️ THE CARD'S OWN CHILDREN, and every one of them is pinned. Only the words
   at the foot may give way, and `fitRecordCard()` shrinks the record to make
   whatever room is needed. */
.rec-card > .rec { flex: 0 0 auto; }

/* ---- under the record -----------------------------------------------------
   Nick, 25 Sep 2026: *"centre the start button under the middle (where the pin
   would go in the middle of the record) … shorten the title and meta area to
   just go to the edge of the album cover … vertically align the play button
   centrally between the title and the metadata row."*

   ⚠️ THE WORDS SIT UNDER THE SLEEVE AND THE CONTROL UNDER THE RECORD. Both
   come from the same custom properties that place the record itself, so nothing
   here is a magic number and nothing needs a breakpoint:

     the title and band stop at  --rec-sw          (the sleeve's edge)
     the button's centre is at   the middle of the EXPOSED part of the record,
                                 (--rec-sw + --rec-dl + --rec-dw) / 2

   ⚠️ THE EXPOSED PART, NOT THE HOLE. Nick, 25 Sep 2026: *"the start button
   should align centre with the whole exposed part of the record not with the
   hole in the record."* The spindle sits BEHIND the sleeve's edge — about
   three-quarters of the way in — so a button lined up on it looked pushed left
   and crowded the title. What the eye reads as the centre is the middle of the
   part it can actually see.

   Change the geometry in `RECORD_*` and all three move together. */
/* ⚠️ `.rec-foot` IS GONE, 26 Sep 2026, with the row it held. It existed to give
   the absolutely-positioned Start button a floor to hang in; the button is in
   the stage now, so the row has nothing left to do. Its `min-height: 74px` was
   74px of empty card under every record. Deleted rather than left empty: a
   leftover `.withart` phone rule silently defeated two attempts at sizing the
   artwork on 24 Sep, and dead CSS is not inert. */
/* ⚠️ `.rec-title` IS GONE, with the line under the cover it styled. The title
   lives on the label alone now — see `RECORD_LAB` in app.js. Its rules are
   deleted rather than left: a leftover `.withart` phone rule silently defeated
   two attempts at sizing the artwork on 24 Sep, and dead CSS is not inert. */

/* ⚠️ THE START BUTTON'S CORNER, from 26 Sep 2026. Nick: *"in line with the
   bottom of the album cover and the right-hand edge... It's just there in that
   little corner."*

   It is in `.rec` (the stage) now, not the row below the artwork. Right edge to
   the RECORD's right edge — `--rec-dl + --rec-dw`, which is 99.2% of the card —
   and bottom to the SLEEVE's bottom.

   ⚠️ `bottom: 0`, NOT `top: 57.5%`. The sleeve is 57.5% of the card WIDE with a
   square aspect and the stage is 57.5% of its own width TALL, so the sleeve's
   bottom edge and the stage's bottom edge are the same line. Written as a top
   percentage it resolves against the stage's HEIGHT and lands a third of the
   way up. The arithmetic looked right; only a render showed it. */
.rec-play {
  position: absolute;
  right: calc(100% - (var(--rec-dl) + var(--rec-dw)));
  bottom: 0;
  z-index: 5;
  display: flex; flex-direction: column; align-items: center;
  color: var(--flame);
  margin: 0;
}
/* ⚠️ IT SCALES OR IT LANDS ON THE RECORD. The largest circle that clears the
   disc in that corner is 8.6% of the card's width — computed from the same
   geometry, not eyeballed. That is generous on a wide screen and 33px on a
   390px phone, which is under the 44px a thumb needs. So `max()` holds 44px at
   the small end and the button OVERLAPS the record's edge by a few pixels
   there. Nick, asked directly: *"If we have to have the stop button overlapping
   the record, that's okay... That's a good compromise."* Of the two faults, too
   small to press is the worse one. */
.rec-play .start { width: max(44px, 8.6%); }
.rec { position: relative; width: 100%; }
/* The record's name, for a screen reader only — the label that shows it is a
   drawing. ⚠️ Not `display: none`, which is not announced at all. */
.rec-name {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.rec-sleeve {
  position: relative; width: var(--rec-sw); aspect-ratio: 1; z-index: 3;
  background: rgba(242, 227, 199, 0.07);
}
.rec-sleeve img,
.rec-sleeve video {
  width: 100%; height: 100%; object-fit: cover; display: block;
  /* ⚠️ THE SIDEWAYS SHADOW IS THE WHOLE ILLUSION. Without it the record reads
     as sitting BESIDE the sleeve rather than inside it — a flat overlap could
     be either, and the eye picks the wrong one. */
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.5), 17px 0 30px -5px rgba(0, 0, 0, 0.82);
}
/* the wear along the opening edge of a sleeve that has been used */
.rec-sleeve::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 2;
  background:
    linear-gradient(105deg, rgba(255, 255, 255, 0.1) 0 2px, transparent 3px),
    radial-gradient(120% 90% at 8% 50%, rgba(0, 0, 0, 0.34), transparent 62%);
}
.rec-disc {
  position: absolute; top: 50%; left: var(--rec-dl); width: var(--rec-dw);
  transform: translateY(-50%); aspect-ratio: 1; border-radius: 50%; z-index: 2;
  /* ⚠️ TWO THINGS ADDED 26 Sep 2026, and both are real facts about a pressing
     rather than decoration. Nick asked for the record to be *"more
     photorealistic"* and chose the middle of three treatments.

     THE LEAD-OUT: the smooth ungrooved ring between the last groove and the
     label. Every record has one and its absence is why a drawn record reads as
     a dartboard — the grooves running right up to the paper is the tell.

     THE RIM: a pressing thickens at its edge and takes the light differently
     there. Without it the disc ends at a hard line, which is what a circle
     does and not what an object does.

     ⚠️ The specular sweep was ALREADY HERE, in `::after` below. It is not part
     of this change and should not be added twice. */
  background:
    radial-gradient(circle at 50% 50%, transparent 0 30%,
                    #16120d 30% 33.2%, transparent 33.3%),
    repeating-radial-gradient(circle at 50% 50%, #1d1610 0 1.7px, #382b1e 1.7px 3.4px),
    #1d1610;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6),
              inset 0 0 18px rgba(0, 0, 0, 0.55);
}
.rec-disc::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%; pointer-events: none;
  background: conic-gradient(from 205deg, rgba(255, 255, 255, 0.17), transparent 20%,
    transparent 58%, rgba(255, 255, 255, 0.11) 72%, transparent 86%);
}
/* ⚠️ These four come from `discoStyle()` and must not be typed as numbers.
   `RECORD_LAB` in app.js decides both how big the label is drawn AND how far
   the record slides out, and the two only agree while there is one copy. */
.rec-disc svg {
  position: absolute; overflow: visible;
  width: var(--rec-lab, 48%); height: var(--rec-lab, 48%);
  left: var(--rec-labo, 26%); top: var(--rec-labo, 26%);
}
.rec-face { fill: var(--flame); }
.rec-hole { fill: #2A1E14; }
/* ⚠️ The ring is NOT decoration. It is the line the sleeve's edge is allowed to
   cut to, and the whole geometry in app.js is written against it, so it has to
   be visible enough to read as a deliberate edge rather than an accident. */
.rec-ring { fill: none; stroke: rgba(42, 30, 20, 0.42); stroke-width: 1.6; }
/* ⚠️ THE LABEL IS SET IN THE DISPLAY FACE, NOT THE HAND, from 26 Sep 2026.
   Nick: *"change the font on the record label itself to not be handwritten...
   use the same font as the one that shows the year."* That is `--display` at
   800, which is exactly what `.rec-year` and `.rec-kind` use — a record label
   is printed matter, and the hand belongs to the things Fergus writes.

   ⚠️ CHANGING THIS CHANGES THE GEOMETRY. `fitLabel()` measures the rendered
   text against the printed ring and shrinks until it fits, so the face is not
   free — a wider face at the same size overflows, and the ring is the line the
   sleeve's edge cuts to, so type past it is EATEN rather than merely crowded.
   Checked by rendering all six records and measuring each title against its own
   ring, not by looking at one. */
.rec-ttl {
  fill: #2A1E14; font-family: var(--display); font-weight: 800;
  letter-spacing: -0.01em; text-anchor: middle;
}

/* ---- the two animations ---------------------------------------------------
   ⚠️ EVERYTHING IS VISIBLE AT REST. Without `.go` the record sits fully out and
   the title is fully written — so no JavaScript, a webfont that never arrives,
   or a phone set to reduce motion all leave a correct page. An animation must
   never be the reason a title is missing, and that is exactly how it happens:
   something starts from nothing and never finishes.

   ⚠️ The title reveal is a WIPE, not real handwriting — the word is uncovered
   left to right; the pen does not follow the letter strokes. Genuine
   stroke-order writing needs the title as a drawn path, and these titles come
   out of the database. At this speed, in this face, the difference is invisible.
   It would not be if it ran slowly. */
/* ⚠️ A RECORD THAT HAS NOT PLAYED YET SITS IN ITS SLEEVE. Added by script, not
   by the markup — see `wireDisco()`. Without it a card is drawn at rest, with
   the record fully out, and arming it makes the record jump BACK into the
   sleeve before sliding out again, because the animation's first frame is the
   tucked position. Nick saw exactly that on every record after the first.

   ⚠️ `.tuck` must never be written into the HTML. No script, no tuck, and the
   page is still correct with the records sitting out. */
.rec.tuck .rec-disc { transform: translateY(-50%) translateX(var(--rec-from-px, 0px)); }

@keyframes rec-slide {
  from { transform: translateY(-50%) translateX(var(--rec-from-px, 0px)); }
  to   { transform: translateY(-50%) translateX(0); }
}
@keyframes rec-write { from { width: 0; } to { width: var(--rec-w, 200px); } }
rect.rec-wipe { width: 200px; }
.rec.go .rec-disc { animation: rec-slide var(--rec-slide, 1.2s) cubic-bezier(0.25, 0.46, 0.45, 0.94) both; }
.rec.go rect.rec-wipe { animation: rec-write 0.5s ease-out both; }

/* ---- the record turns once you press Listen -------------------------------
   ⚠️ AFTER the `.go` rule on purpose. Both select at the same weight and both
   animate `transform`, which does not compose — the later rule wins, and the
   spin has to be the one that does. The slide has finished by then anyway.

   ⚠️ `translateY(-50%)` is carried through every keyframe. Leave it out and the
   record jumps half its own height up the card the moment it starts turning. */
@keyframes rec-spin {
  from { transform: translateY(-50%) rotate(0deg); }
  to   { transform: translateY(-50%) rotate(360deg); }
}
/* ⚠️ `.turning`, NOT `.playing`. The record starts to turn only once the
   chooser has finished sliding in — Nick: *"can the spinning start after the
   widget has slid in?"* `.playing` goes on at the press and is the STATE (the
   lamp, and what a stop press reads); `.turning` is the last of the three
   moments. Conflating them made a stop press in the first second restart the
   record. */
.rec.turning .rec-disc { animation: rec-spin 1.8s linear infinite; }
@media (prefers-reduced-motion: reduce) {
  .rec.go .rec-disc { animation: none; }
  .rec.go rect.rec-wipe { animation: none; width: 200px; }
  /* ⚠️ A record turning for ever is the single worst thing on this page for
     somebody who has asked for less motion — it never stops. */
  .rec.turning .rec-disc { animation: none; }
}

/* ---- the band, and the three labels that lead into things ----------------- */
/* ⚠️ THE BAND IS THE FULL WIDTH OF THE CARD, from 26 Sep 2026, and it holds
   the folds. Nick: *"put them inside, in between the two lines where it says EP
   2026... when those pop open, what's there appears in the lines, between the
   lines."* It used to stop at `--rec-sw` with the Start button filling the rest
   of that row; the button has moved into the stage, so the width is free.

   ⚠️ `flex-wrap` IS WHAT MAKES THIS WORK WITHOUT A SCRIPT. Shut, every fold is
   an `auto` item riding the same row as the kind and the year. Open, the rule
   below gives one a 100% basis, so it takes its own full-width row inside the
   two rules and its content opens under its own summary. No handler, and
   `<details>` keeps its keyboard, its announcement and its find-in-page. */
.rec-band {
  display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
  width: 100%;
  margin: clamp(5px, 0.8vh, 9px) 0 0; padding: 9px 0;
  border-top: 1.5px solid var(--faint); border-bottom: 1.5px solid var(--faint);
}
/* ⚠️ THE BAND HOLDS NO FOLDS ANY MORE, from 26 Sep 2026 — Tracks moved onto
   the sleeve and Listen stopped being a fold at all. The flex-wrap rules that
   gave an open fold its own full-width row are deleted rather than left: dead
   CSS is not inert here, and a rule matching nothing is a rule the next person
   has to work out the purpose of. The band is a flex row of the kind, the year
   and the players' slot.
   ⚠️ The slot takes the full width when it is filled, which is what lets the
   two players sit side by side. */
.rec-band > .rec-listen { flex: 1 1 100%; }
.rec-band > .rec-listen:empty { display: none; }
.rec-kind {
  font-family: var(--display); font-weight: 800; font-size: clamp(12px, 1.5vw, 15px);
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--flame);
}
.rec-year {
  font-family: var(--display); font-weight: 800; font-size: clamp(12px, 1.5vw, 15px);
  letter-spacing: 0.1em; font-variant-numeric: tabular-nums;
}
.rec-run {
  margin-left: auto; font-size: 10px; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--dim);
}
.rec-run b {
  font-family: var(--display); font-weight: 600; font-size: 14px;
  letter-spacing: 0; text-transform: none; color: var(--ink); margin-left: 7px;
}
/* ABOUT: / TRACKS: / LISTEN: — one voice, each leading into its own content. */
.rec-cue {
  font-family: var(--display); font-weight: 800; text-transform: uppercase;
  letter-spacing: 0.14em; color: var(--flame); font-size: 10.5px;
  margin-right: 7px; white-space: nowrap;
}
.rec-about {
  font-family: var(--hand); font-size: clamp(15px, 1.9vh, 17px); line-height: 1.5;
  margin: clamp(10px, 1.6vh, 16px) 0 0; max-width: 52ch;
}
.rec-tracks {
  font-size: 13px; line-height: 1.75; color: var(--dim);
  margin: clamp(8px, 1.4vh, 14px) 0 0; max-width: 62ch;
}
/* ⚠️ A number and its title break as ONE unit, or a number strands at the end
   of a line. The spaces around the slash in app.js are then what let the line
   wrap at all — without them it has no break opportunity and runs off. */
.rec-tracks .rec-tk { white-space: nowrap; }
.rec-tracks .rec-n { color: var(--flame); font-weight: 600; }
.rec-tracks .rec-sep { color: var(--faint); }
/* ⚠️ CENTRED, and it is a DRAWN OUTLINE rather than a filled button. Nick,
   25 Sep 2026: *"a drawn orange box (not coloured in) that says 'Listen
   everywhere', centred underneath each one."*

   The colour is set here rather than on the box, because `boxContent()` strokes
   with `currentColor` — so one declaration turns the outline and the word
   orange together and they cannot drift apart. */
/* ⚠️ `margin-top: auto` PINS IT TO THE FOOT of a card that is now as tall as
   the tallest on the shelf — which is what makes the boxes line up. */

.rec-play .playon { margin: 0; }
.rec-play .door-cta,
.rec-play .door-word { color: var(--flame); }
/* ---- the start button, in chrome ------------------------------------------
   Nick, 25 Sep 2026: *"start button, in chrome."*

   ⚠️ THE CHROME IS A GRADIENT, not a picture — no asset, nothing to load, and
   it scales with the card. The bands are what make metal read as metal: light,
   dark, light again across the face. A flat grey circle reads as a disabled
   button; the bands are the whole difference. */
.start {
  display: inline-flex; align-items: center;
  text-decoration: none; color: var(--ink);
}
.start-knob {
  display: block; width: 44px; height: 44px; flex: 0 0 44px;
  border-radius: 50%;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.55), 0 1px 0 rgba(255, 255, 255, 0.18) inset;
  transition: transform 0.14s ease, box-shadow 0.14s ease;
}
.start-knob svg { width: 100%; height: 100%; display: block; }
/* ⚠️ THE LAMP IS THE STATE, and the word beside it is the ACTION. Nick:
   *"maybe play is a little red light on and stopped is light off?"* Off means
   stopped, lit means playing — which is what every piece of equipment with a
   power light has always meant, and needs no explaining.

   ⚠️ Red, not the site's orange. This is the one thing on the page that is a
   LAMP rather than an accent, and at 7px across on a chrome face the accent
   orange reads as a smudge of rust. */
.start .lamp { fill: rgba(28, 22, 16, 0.6); transition: fill 0.3s ease, filter 0.3s ease; }
.start.lit .lamp {
  fill: #FF2D16;
  filter: drop-shadow(0 0 3px rgba(255, 45, 22, 0.95));
}
.start:hover .start-knob { transform: translateY(-1px); }
.start:active .start-knob,
.start.lit .start-knob {
  transform: translateY(2px);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6), 0 2px 5px rgba(0, 0, 0, 0.45) inset;
}
/* ⚠️ PLINTH LETTERING. Nick: *"style the writing like on a chrome and dark
   wood record player."*

   Every part of this is doing that one job:

   - SMALL AND WIDELY SPACED. Legends on a turntable's plinth are set tiny and
     opened right out — it is the spacing, more than the face, that says
     "equipment" rather than "website".
   - CHROME, NOT THE ACCENT. The site's orange would make it a link. This is a
     word printed on a machine, so it takes the colour of the machine.
   - ENGRAVED, NOT PRINTED. A dark shadow below the letters and a faint light
     one above is what cut-into-wood looks like; reverse the two and it would
     read as raised.
   - ⚠️ `text-indent` MATCHES THE LETTER-SPACING. Letter-spacing adds a gap
     after the LAST letter too, so a centred word at 0.34em sits visibly left
     of centre. The indent puts it back. At this spacing it is about 4px, which
     is small and is exactly the kind of thing that looks like a wobble. */
.start-cue {
  font-family: var(--display); font-weight: 800;
  font-size: 10px; letter-spacing: 0.34em; text-transform: uppercase;
  color: #CFC7B8;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.8), 0 -1px 0 rgba(255, 255, 255, 0.09);
  margin: 10px 0 0; text-align: center; text-indent: 0.34em;
}

/* ---- the widget, once it has been asked for -------------------------------
   ⚠️ It slides in from where the box was, not down from above: the card's
   height is fitted to the panel and the site does not scroll, so anything that
   grows downwards pushes the foot off the screen.

   The slot keeps the box's height while the frame arrives, so nothing jumps.
   `fitRecordCard()` runs after the swap as well, because the frame and the box
   are not the same height. */
/* ---- the chooser, across the foot of the artwork ---------------------------
   Nick, 25 Sep 2026: *"the widget slides in over the bottom of the album cover
   so that it doesn't interfere with anything below."*

   ⚠️ THIS REMOVES THE PREVIOUS VERSION'S WHOLE PROBLEM rather than working
   around it. Over the WORDS it had to hide the title, the band and the track
   line — and a scrim could not be trusted to cover them, because `.rec-meta`
   measured 117px while its own children measured 147 and they drew below the
   box it was positioned against. Over the ARTWORK there is nothing to hide and
   nothing to measure. Everything below stays put and stays readable, so both
   the scrim-lifting of the title and the `visibility: hidden` that went with it
   are deleted rather than left as rules that do nothing.

   ⚠️ `display: none` until it opens, so nothing can be tabbed to through a
   panel that is not there. */
.rec-cover { display: none; }
.rec[data-open="1"] .rec-cover {
  display: flex; align-items: center;
  position: absolute; left: 0; bottom: 0; z-index: 6;
  width: var(--rec-sw);
  padding: 10px;
  /* Fading up into the artwork rather than a hard edge — it is sitting ON the
     cover, not replacing the bottom of it. */
  background: linear-gradient(to top, rgba(20, 14, 9, 0.97) 62%, rgba(20, 14, 9, 0));
}

.odesli-in {
  width: 100%;
  /* ⚠️ Read back from `SOUND.slideIn`, because the moment the record starts
     turning is timed from the same figure. Hard-code it here and the two drift
     the first time anyone retunes the sequence. */
  animation: rec-slidein var(--rec-slide-in, 0.45s) cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
  overflow: hidden;
}
.odesli-in iframe {
  width: 100%; height: 52px; border: 0; display: block;
  border-radius: 8px;
}
@keyframes rec-slidein {
  from { opacity: 0; transform: translateX(22px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .odesli-in { animation: none; }
}

/* Wide enough for two records to show, so the boxes have something to line up
   with: pin them to the foot of a card that is as tall as the tallest. */
/* ⚠️ The rule that pinned this to the foot of the card is gone with the layout
   that needed it — the button is in the title row now, which puts it level on
   every card without anything having to be stretched to match. */

/* ⚠️ The visible `.rec-title` is gone, 25 Sep 2026 — the label carries the name
   on its own now. What remains is the HIDDEN heading below, which a screen
   reader needs because the label is a drawing. */

/* ⚠️ THE OLD DECK'S RULES ARE GONE, 25 Sep 2026 — `.deck { display:flex }`,
   `.deck > .sleeve-body`, `.deck-nav`, `.arrow` and `.deck-title`. They styled
   the one-card-at-a-time deck and its arrows, which nothing renders any more.

   ⚠️ **`.deck` had to go rather than merely being unused**: the tape deck
   further down this file claims the same class name, and the two were both
   applying — a flex column with `flex: 0 1 auto` was being laid over a machine
   that is not a flex column. Two rules for one name, doing different things,
   is not dead code sitting quietly. */
.dots { display: flex; gap: 7px; align-items: center; }
.dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--faint); flex: 0 0 auto;
}
.dot.on { background: var(--flame); }

/* A playlist with no artwork yet gets its initial, at sleeve size. */
.art-big.art-none {
  display: grid; place-items: center;
  width: 100%; aspect-ratio: 1;
  background: var(--wash, rgba(51,32,15,0.06));
  font-family: var(--hand); font-size: 40px; color: var(--faint);
}

/* ---- the Odesli row -------------------------------------------------------
   Nick, 24 Sep 2026: one per release, not one per song. See the note on
   `odesli()` for why the per-song version was rejected after rendering it. */
.odesli { margin-top: clamp(8px, 1.6vh, 18px); max-width: 420px; }
.odesli iframe {
  display: block; width: 100%; height: 52px; border: 0; border-radius: 8px;
}
/* ⚠️ It is somebody else's widget and it has its own idea of how wide to be.
   Held to the sleeve's measure so it cannot become the widest thing on the
   panel — which on a phone it otherwise would. */
@media (max-width: 720px) { .odesli { max-width: none; } }

/* ---- "there is more below" -------------------------------------------------
   Nick, 24 Sep 2026: a layout that *"shows at least the first three songs,
   allows a scroll, makes it obvious that there's a scroll"*.

   ⚠️ ONLY WHEN THERE ACTUALLY IS MORE. `data-more` is set by `markScrollable()`
   after layout — a fade drawn unconditionally lies on every single that has
   three tracks and nothing hidden, which is most of them. */
.sleeve-words[data-more] {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 34px), transparent);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 34px), transparent);
}
/* And a word, because a fade alone is a thing people miss. ⚠️ Drawn by the
   PARENT, not by the scroller — a hint inside the masked element is faded out
   by the very thing it is explaining. */
.sleeve-body[data-more]::after {
  content: 'more below \2193';
  flex: 0 0 auto;
  font-family: var(--hand); font-size: 13px;
  color: var(--flame);
  margin-top: -2px;
  pointer-events: none;
}


/* ⚠️ THE BANNER'S RULES ARE GONE, 25 Sep 2026, one day after they arrived. The
   artwork is the panel's BACKGROUND now — see `plateKeyFor()` in app.js — so
   there is nothing in the panel to size. Removed rather than left unused,
   because a leftover `.withart` rule silently held the artwork down through
   two attempts the day before. Dead CSS here has a track record. */

/* ---- the slow drift -------------------------------------------------------
   Nick, 25 Sep 2026: *"let's do a slow drift as default for still image across
   the site."*

   A still photograph creeps and scales very gently, the way a documentary pans
   across one. It costs no file and nothing to make, and it applies to every
   picture on the site — including covers Fergus uploads in a year.

   ⚠️ STILLS ONLY. A video already moves; drifting it as well would be two
   motions fighting, and the films behind these panels are hand-held already. */
@keyframes drift {
  from { transform: scale(1.07) translate3d(-1.1%, -0.7%, 0); }
  to   { transform: scale(1.13) translate3d(1.1%, 0.7%, 0); }
}
.plate img {
  /* ⚠️ `alternate`, NOT a plain loop. A 50-second pan that snaps back to its
     start is a jump cut once a minute — the one frame anybody would notice in
     the whole effect. Reversing is invisible. */
  animation: drift 52s ease-in-out infinite alternate;
  /* ⚠️ THE SCALE NEVER GOES BELOW 1.07 while the translate reaches 1.1%. It
     has to stay comfortably ahead, or the picture walks far enough to show the
     panel behind its edge — a hairline of background sliding in and out along
     one side, which reads as a rendering fault rather than a pan. */
  will-change: transform;
}

/* ⚠️ THE REDUCED-MOTION BLOCK ABOVE WOULD MAKE THIS WORSE, NOT BETTER, and
   that is why this rule exists rather than relying on it.

   That block sets `animation-duration: 0.001s !important` on everything — which
   for a one-shot animation means "land on the end state instantly", exactly
   right. But this one is `infinite alternate`: at 0.001s it would flip between
   its two ends about a thousand times a second. **A setting asking for less
   motion would produce a strobing photograph.**

   So the drift is switched off by name, and the picture sits still at the size
   it starts. */
@media (prefers-reduced-motion: reduce) {
  .plate img { animation: none !important; transform: scale(1.07); }
}

/* ⚠️ THE TWO LISTS OF VIDEOS ARE GONE, 25 Sep 2026, and their rules with them
   — `.films`, `.film`, `.film-wrap`, `.film-name`, `.vid-part`, `.vid-head`.
   The television replaced them: one screen whose player is built inside the
   press, and a strip of stills under it. See `.tv` further down. */

/* ---- the tape, without the deck -------------------------------------------
 *
 * Nick, 25 Sep 2026: *"the reason the record player looks cool is because we're
 * not actually showing a record player; we're just hinting at it… the tape is
 * there and it just spins but there's not necessarily a tape deck, just the
 * tape and a play button."*
 *
 * ⚠️ THIS DELETES A WHOLE MACHINE BUILT THE SAME DAY — the cabinet, the speaker
 * grille, the well, the moulded lip, the legend and six keys of which five did
 * nothing. It was more convincing and less good: every part had to be drawn and
 * none of it was the point.
 *
 * ⚠️ THE RECORD PANEL IS THE PRECEDENT and Nick named it: there is no turntable
 * on it, only a sleeve, a disc pulled half out, and one chrome knob. **One
 * piece of hardware is what makes the rest read as a machine.** Here it is four
 * marks under a cassette.
 * -------------------------------------------------------------------------- */

.tapes { margin: 0 auto; width: min(100%, 430px); }

/* ⚠️ THE SHELF IS THE PAGE NOW, not a well inside a box. Same mechanism as the
   records — a snap point per tape — but nothing is drawn around it. */
.well {
  position: relative; display: flex; gap: 18px;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory; scrollbar-width: none;
  padding: 2px 0 4px;
}
.well::-webkit-scrollbar { height: 0; width: 0; }
.well:focus-visible { outline: 2px solid var(--flame); outline-offset: 4px; }
.tape-slot { flex: 0 0 100%; scroll-snap-align: center; min-width: 0; }

/* ⚠️ THE SHELL HAS TO BE LIGHTER THAN THE GROUND IT SITS ON, or the cassette
   disappears into the page. It used to sit in a dark well, which did that job;
   with the well gone the shell carries its own separation, through the shadow
   under it. */
.tape {
  position: relative; aspect-ratio: 100 / 64; border-radius: 6px;
  background: linear-gradient(158deg, #6A625A, #4A443E 46%, #565049 78%, #3E3934);
  box-shadow: 0 16px 34px rgba(0, 0, 0, 0.6), 0 1px 0 rgba(255, 255, 255, 0.28) inset;
  padding: 7px 8px;
}
/* ⚠️ THE RIBBING, added 26 Sep 2026 — the fine vertical moulding lines down a
   cassette shell. Nick chose the middle of three treatments for the tape.

   ⚠️ IT IS AN OVERLAY, NOT PART OF THE SHELL GRADIENT, and that is the point:
   there are FIVE shell colours and the ribbing has to sit on all of them
   without being written five times. Putting it in `.tape`'s own background
   would have meant editing every `.shell-n` rule and keeping them in step.

   ⚠️ `pointer-events: none` because it covers the whole tape, keys included. */
.tape::before {
  content: ''; position: absolute; inset: 0; border-radius: 6px; z-index: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(90deg,
      rgba(0, 0, 0, 0.055) 0 2px, rgba(255, 255, 255, 0.022) 2px 5px),
    linear-gradient(156deg, rgba(255, 255, 255, 0.10) 0%, transparent 30%);
}
/* everything real on the tape has to sit above it */
.tape > * { position: relative; z-index: 1; }
/* ⚠️ FIVE SHELLS, IN THE SAME FAMILY. Nick, 25 Sep 2026: *"could we make the
   tapes different colours, the mix tapes SLIGHTLY different colours?"* —
   slightly is the whole instruction. These are five shades of moulded plastic
   out of one drawer, not five colours: grey-brown, warm grey, olive, slate and
   a faded red. A rack of cassettes, which is what a shelf of mix tapes is.

   ⚠️ THE SHELL IS PICKED BY THE ROW'S POSITION, not at random and not from a
   new column — see `tapeSlot()`. Random would change on every render and read
   as a fault. */
.tape.shell-0 { background: linear-gradient(158deg, #6A625A, #4A443E 46%, #565049 78%, #3E3934); }
.tape.shell-1 { background: linear-gradient(158deg, #6E6157, #4E443B 46%, #5A4F45 78%, #413830); }
.tape.shell-2 { background: linear-gradient(158deg, #5F6356, #42473C 46%, #4E5347 78%, #383C32); }
.tape.shell-3 { background: linear-gradient(158deg, #5C626A, #40454C 46%, #4C5158 78%, #363A40); }
.tape.shell-4 { background: linear-gradient(158deg, #6E5A55, #4C3E3A 46%, #584946 78%, #3F3431); }

/* ⚠️ FIVE SCREWS, NOT FOUR. A Compact Cassette has one in each corner AND one
   in the middle of the bottom edge — one of those details nobody can name and
   everybody notices the absence of. */
.screw {
  position: absolute; width: 5px; height: 5px; border-radius: 50%;
  background: radial-gradient(circle at 34% 32%, #9A9288, #3A3630 70%, #5A544C);
}
.screw::after {
  content: ''; position: absolute; inset: 1.4px; border-radius: 50%;
  background: linear-gradient(90deg, transparent 42%, #2A2620 42% 58%, transparent 58%);
}
.screw.a { left: 6px; top: 6px; }   .screw.b { right: 6px; top: 6px; }
.screw.c { left: 6px; bottom: 6px; } .screw.d { right: 6px; bottom: 6px; }
.screw.e { left: 50%; bottom: 5px; margin-left: -2.5px; }
.ribs {
  position: absolute; left: 24%; right: 24%; top: 4px; height: 3px;
  background: repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.13) 0 1px, transparent 1px 4px);
}
.tape-side {
  position: absolute; right: 10px; top: 7px; font-family: var(--display); font-weight: 700;
  font-size: 7px; letter-spacing: 0.18em; color: rgba(255, 255, 255, 0.42);
}

/* ---- the written-on label ---- */
.tape-label {
  position: relative; height: 41%; margin: 7px 5px 0; border-radius: 1px;
  padding: 15px 9px 0;
  background: linear-gradient(180deg, #F7F1E2, #E4D9BF);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(0, 0, 0, 0.35);
  overflow: hidden;
}
/* ⚠️ THE COLOURED HEADER STRIP is the single most recognisable thing about a
   cassette label — every brand had one, and without it the sticker reads as a
   blank card. */
.tape-label::before {
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 10px;
  background: linear-gradient(90deg, #C0392B, #D9541F 55%, #B03A16);
}
.tape-label::after {
  content: 'A'; position: absolute; left: 6px; top: 0; height: 10px;
  font-family: var(--display); font-weight: 700; font-size: 7.5px; line-height: 10px;
  color: rgba(255, 255, 255, 0.9);
}
.tape-label .rule { position: absolute; left: 9px; right: 9px; height: 1px; background: rgba(40, 30, 20, 0.22); }
.tape-label .rule.one { top: 31px; } .tape-label .rule.two { top: 49px; }
/* ⚠️ THE INK IS BIRO BLUE AND DOES NOT FOLLOW THE PAGE'S PALETTE. Everything
   else on this site flips pale on the dark ground; a cassette label is a
   sticker with a pen on it and is lit by nothing. */
.tape-name {
  margin: 0; font-family: var(--hand); color: #1F3C6E; font-size: 15px; line-height: 1.35;
  position: relative; z-index: 2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* ⚠️ CLAMPED AT TWO LINES, and that is the real limit on how much Fergus can
   write on a tape. A long note is CUT rather than overflowing the cassette. */
.tape-note {
  margin: 4px 0 0; font-family: var(--hand); color: rgba(31, 60, 110, 0.78);
  font-size: 12px; line-height: 1.4; position: relative; z-index: 2;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.tape-note .empty {
  color: var(--flame); font-family: var(--body); font-size: 8px; letter-spacing: 0.14em;
  text-transform: uppercase; border: 1px solid var(--flame); border-radius: 2px; padding: 0 4px;
}

/* ---- the window with the reels ----
   ⚠️ THREE THINGS MAKE IT READ AS TAPE: a dark window, two spools of DIFFERENT
   size — one nearly full, one nearly empty, which is what says a tape has been
   played — and pale hub teeth you can see turning. Without the size difference
   two identical circles read as speaker holes. */
.tape-window {
  position: absolute; left: 9%; right: 9%; bottom: 7%; height: 40%;
  border-radius: 3px; background: linear-gradient(180deg, #120F0D, #1E1916);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.14) inset, 0 2px 5px rgba(0, 0, 0, 0.6) inset;
  display: flex; align-items: center; justify-content: space-between; padding: 0 6%;
}
.tape-window::after {
  content: ''; position: absolute; left: 14%; right: 14%; bottom: 16%; height: 2px;
  background: #0A0807; border-radius: 1px;
}
.hub { position: relative; width: 42%; aspect-ratio: 1; border-radius: 50%; }
.hub.left  { background: radial-gradient(circle at 50% 50%, #1D1815 0 30%, #2A2320 30% 33%, #15110F 33% 92%, transparent 92%); }
.hub.right { background: radial-gradient(circle at 50% 50%, #1D1815 0 30%, #2A2320 30% 33%, #15110F 33% 54%, transparent 54%); }
.hub .teeth {
  position: absolute; inset: 30%; border-radius: 50%;
  background:
    conic-gradient(#C9BEB0 0 14deg, transparent 14deg 60deg, #C9BEB0 60deg 74deg,
      transparent 74deg 120deg, #C9BEB0 120deg 134deg, transparent 134deg 180deg,
      #C9BEB0 180deg 194deg, transparent 194deg 240deg, #C9BEB0 240deg 254deg,
      transparent 254deg 300deg, #C9BEB0 300deg 314deg, transparent 314deg),
    #3A332D;
}
@keyframes tape-reel { to { transform: rotate(360deg); } }
.tape-slot.playing .teeth { animation: tape-reel 1.1s linear infinite; }
.tape-slot.playing .hub.left .teeth { animation-duration: 1.4s; }
/* ⚠️ Spooling is the SAME keyframe five times faster. One animation, two
   speeds — a second keyframe would be the same fact written twice. */
.tape-slot.spooling .teeth { animation: tape-reel 0.22s linear infinite; }

.tape-edge { position: absolute; left: 0; right: 0; bottom: 0; height: 12%; pointer-events: none; }
.tape-edge .head {
  position: absolute; left: 34%; right: 34%; bottom: 0; height: 72%;
  background: #100D0B; border-radius: 2px 2px 0 0;
}
.tape-edge .pad {
  position: absolute; left: 44%; right: 44%; bottom: 12%; height: 34%;
  background: #5C5048; border-radius: 1px;
}
.tape-edge .capstan {
  position: absolute; bottom: 20%; width: 5%; aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle at 40% 36%, #15120F 0 52%, #453E37 52%);
}
.tape-edge .capstan.l { left: 15%; } .tape-edge .capstan.r { right: 15%; }

/* ---- the transport, and it is ONLY the icons ----
   ⚠️ NO PLINTH, NO LIP, NO KEYS. Four marks under the tape, the way the record
   has one knob under the sleeve. They are shapes everybody already knows, so
   nothing has to be labelled — the opposite of the deck this replaced, where
   five keys had to be left BLANK because naming them would have been five
   promises the machine could not keep. */
.transport {
  display: flex; align-items: center; justify-content: center;
  gap: clamp(20px, 6vw, 40px); margin: 24px 0 0;
}
.tkey {
  border: 0; background: none; padding: 8px; cursor: pointer;
  color: rgba(242, 227, 199, 0.82);
  display: grid; place-items: center; line-height: 0;
  transition: color 0.15s ease, transform 0.09s ease;
}
.tkey svg { width: 26px; height: 26px; fill: currentColor; display: block; }
.tkey.big svg { width: 36px; height: 36px; }
.tkey.big { color: var(--flame); }
.tkey:hover { color: var(--ink); }
.tkey:active { transform: translateY(2px); }
.tkey:focus-visible { outline: 2px solid var(--flame); outline-offset: 3px; }
/* ⚠️ Faded, not hidden. A key that disappears at the end of the shelf makes
   the row jump about — the same call the deck's arrows made before them. */
.tkey[disabled] { opacity: 0.28; cursor: default; }

/* ⚠️ THE DOTS ARE GONE, 25 Sep 2026 — Nick: *"remove the little dots to
   signify how many playlists there are."* What they really did was say where
   you were on the shelf, and the transport keys do that now: each greys out at
   its end. One thing saying it, not two.

   ⚠️ The keyboard argument they were kept for still holds, and is still met:
   the shelf itself is focusable and the two keys move it. */

/* ⚠️ `.deck-state` IS GONE with the line it styled — Nick, 25 Sep 2026:
   *"where it says 'playing on Spotify', remove that."* What it said while the
   tape ran is said by the reels turning, and what it said afterwards is said
   by the player appearing. */

/* ---- where listening happens ----------------------------------------------
 *
 * ⚠️ THESE RULES WERE LOST AND WRITTEN BACK. Taking the dots out sliced from
 * one marker to another and swept up everything between them — the tape's
 * player, its way out, and the record's whole listen block. **Third time in one
 * afternoon that an index-to-index slice took more than it was aimed at.**
 * Nothing threw; the panels simply had no styling.
 *
 * The lesson is now a rule: **a slice is only safe when you have read what is
 * between the two ends**, and cheaper still is to replace a named block rather
 * than a range.
 * -------------------------------------------------------------------------- */

/* ⚠️ ONE PLAYER, UNDER THE KEYS — not one inside each cassette, which is where
   it was until 25 Sep 2026. Nick: *"make the Spotify player and 'Open this in
   Spotify' appear below the play controls."* Inside the tape it scrolled
   sideways with the shelf and sat ABOVE the keys that had summoned it. */
.tape-player { position: relative; margin: 18px 0 0; }
.tape-player:empty { display: none; }
.tape-player .player { max-width: none; }
.tape-player .spot-holder + iframe,
.tape-player iframe { display: block; width: 100%; border: 0; border-radius: 8px; }

/* ⚠️ THE WAY OUT OF THE EMBED. Nick: *"can it also show 'Open this in Spotify'
   … so that you get the sample, but then you can jump out to play elsewhere."*
   There is no "elsewhere" for a playlist — Odesli has no useful answer for one
   — so a tape gets the link that is real and nothing else. */
.tape-out { margin: 10px 0 0; text-align: center; }
.tape-out:empty { display: none; }

/* ---- the record's listen block, under the record ----
   ⚠️ IT MOVED OUT OF THE ARTWORK, 25 Sep 2026. Nick reversed his own earlier
   decision on seeing it: *"maybe it could go under the play button."*

   ⚠️ WHAT WAS WRONG WAS THE WIDTH, NOT THE POSITION. Over the artwork the block
   was capped at the SLEEVE's width, and Spotify's signed-out panel — "Listen to
   these tracks & millions more on Spotify" — wrapped and was cut off mid-word.
   Here it has the card, so the sentence fits on one line. */
/* ⚠️ THE CAP CAME OFF, 26 Sep 2026. 520px was right when the players were
   stacked under a sleeve-width column; side by side they need the card. */
.rec-listen { margin: 0; max-width: none; }
.rec-listen:empty { display: none; }
/* ⚠️ EACH ONE IS NAMED. Nick: *"it's kind of like, 'here it is in Spotify, here
   it is everywhere else'."* Two dark rounded boxes stacked with nothing said
   about either is what he was looking at. */
.listen-cue {
  margin: 0 0 5px; font-family: var(--display); font-weight: 700;
  font-size: 9px; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--dim);
}
.rec-listen .listen-cue ~ .listen-cue { margin-top: clamp(10px, 1.6vh, 16px); }

/* ⚠️ THE SLOT SPOTIFY'S CONTROLLER TAKES OVER. It replaces this element with
   its own iframe, so the rules sit on the PARENT — a class on the holder is
   gone the moment the controller arrives. */
.rec-spot { margin: 0 0 8px; }
/* ⚠️ AN ARMED PLAYER IS CLIPPED, NEVER HIDDEN — and the distinction is the
   whole feature. `display: none` or `visibility: hidden` would stop it being a
   player: a browser will not keep a frame it is not showing ready to make
   noise, which is exactly what this frame exists to be.

   Clipped to nothing, it is loaded, laid out and present — and it measures
   ZERO, so `fitRecordCard()` sizes the card as though the slot were still
   empty, which is what it looked like before this existed. The class comes off
   at the press and the player is simply there.

   ⚠️ `margin: 0` too. An 8px margin on a clipped box is 8px of nothing, which
   would open a gap under every record that has been armed and not played. */
/* ⚠️ THE CLASS CARRIES THE BEHAVIOUR, NOT THE PARENT — corrected 26 Sep 2026
   and it was a live fault for the tape deck.

   This was written as `.rec-spot.spot-armed, .rec-armed.spot-armed`, naming
   the two containers the record uses. When the deck was armed the same way,
   `#tape-player.spot-armed` matched neither — so the class went on, everything
   *said* it was hidden, and **the player sat there 152px tall for anyone who
   opened Mixtapes without pressing play.**

   ⚠️ AND IT LOOKED CORRECT FROM THE CODE. The class was present, the checks
   passed, and only measuring the element's height showed it. A styling hook
   scoped to the places it was first used is a hook that silently does nothing
   the second time somebody reaches for it. */
.spot-armed { height: 0; margin: 0; overflow: hidden; pointer-events: none; }
.rec-armed { margin: 0; }
.rec-armed iframe,
.rec-elsewhere iframe { display: block; width: 100%; border: 0; border-radius: 8px; }
.rec-elsewhere[hidden] { display: none; }

/* ---- the two tabs -----------------------------------------------------------
 *
 * Nick, 26 Sep 2026: *"maybe both widgets load in tabs with the Spotify showing
 * and the other showing on press of its tab — maximising for compactness."*
 *
 * ⚠️ THE POINT IS THE HEIGHT, NOT THE STYLING. Stacked, the Spotify player and
 * the chooser are about 130px plus two captions, and `fitRecordCard()` pays for
 * that by shrinking the record. One pane at a time is 80px, and the artwork
 * keeps its size. On a phone the second widget was simply below the screen.
 *
 * ⚠️ AND THE STRIP IS TINY ON PURPOSE. A tab row in the site's display face at
 * panel size would cost as much height as the widget it is saving; at 10px with
 * the label in small caps it costs 22px and still reads as two choices.
 */
.rec-tabs {
  display: flex; gap: 14px; align-items: baseline;
  margin: 0 0 6px; padding: 0 1px;
}
.rec-tabs[hidden] { display: none; }
.rec-tab {
  border: 0; background: none; padding: 0 0 3px; cursor: pointer;
  font-family: var(--display); font-weight: 600;
  font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--dim);
  /* The rule under the live tab is drawn in the accent, so which one is showing
     survives the palette flip onto the photograph. */
  border-bottom: 2px solid transparent;
}
.rec-tab:hover { color: var(--ink); }
.rec-tab.on { color: var(--ink); border-bottom-color: var(--flame); }
.rec-spot iframe { display: block; width: 100%; border: 0; border-radius: 8px; }
/* ⚠️ THE SPOTIFY PLAYER NEEDS ITS OWN HEIGHT. `.odesli-in iframe` pins every
   frame in here to 52px, which is right for the chooser and squashes the
   player into an unusable strip. 152 is Spotify's compact player. */
.odesli-in .rec-spotify { display: block; width: 100%; height: 80px; border: 0; margin: 0 0 8px; }
/* ⚠️ AND THE SAME HEIGHT BY THE SLOT, added 26 Sep 2026 — this was a LIVE BUG,
   not a consequence of that day's rearrangement. It was measured at 52px on the
   deployed site before anything was touched.
   
   The rule above targets `.rec-spotify`, which is the class on the FALLBACK
   embed. When Spotify's own controller mounts successfully it replaces
   `.rec-spot` with an iframe of its own that carries NO CLASS — so the rule
   missed it and `.odesli-in iframe`'s 52px won. The player was a 52px strip
   whenever Spotify worked, and the right height only when Spotify failed.
   
   ⚠️ Targeting the SLOT rather than the class is the fix, because the slot is
   ours and the class on what lands in it is not. Anything mounted there is the
   player and wants the player's height. */
/* ⚠️ 80px, NOT 152px, from 26 Sep 2026 — and the change is a FIT fix, not a
   taste one. Un-squashing this player from its broken 52px to Spotify's 152px
   compact size added 100px to an open card, and the Discography panel does not
   scroll: it overran by 104px at 1200x900 and sliced its own bottom off.

   ⚠️ Shrinking the record could not absorb it. The card's width follows the
   record's HEIGHT, so shrinking made the players narrower and never shorter,
   and 152px is fixed — which is why `fitRecordCard()` bottomed out on its floor
   instead of converging.

   ⚠️ AND NOTHING IS LOST AT THIS WIDTH. The 152px player spends its extra
   height on a track list that is clipped mid-row at 288px anyway. The 80px
   player carries the artwork, the artist, the current track and the whole
   transport. Checked by rendering all three heights and looking at them. */
.odesli-in .rec-spot iframe { height: 80px; }

/* ---- the television, back again -------------------------------------------
 *
 * Nick, 25 Sep 2026: *"for the TV, can we go back to the TV format that we had
 * before? That was pretty cool."*
 *
 * ⚠️ THIS REVERSES THE PANEL BEING STRIPPED BACK EARLIER THE SAME DAY, and ONLY
 * for the television. **The tape stays bare** — he kept that one. So the
 * principle he named (*"we're just hinting at it"*) holds for the cassette and
 * not for the set. That is his call, and it is written here so that nobody
 * restores consistency by taking the cabinet off again.
 *
 * ⚠️ WHAT CAME BACK IS THE CABINET ONLY. Everything learned in between stays:
 * three channels, "Next ›" on every key, the mechanical clack, the dimmed snow,
 * and `pointer-events: none` on every layer over the picture — which is the fix
 * for the video that could not be stopped.
 * -------------------------------------------------------------------------- */

/* ⚠️ THESE TWO RULES WERE LOST IN THE RESTORE and had to be written back. The
   slice that brought the cabinet out of git began mid-declaration, so `.tv` and
   the `.tv::before` SELECTOR both vanished — leaving an orphan block of
   declarations with nothing to apply them to. The set rendered as a tube
   filling the page with the channel keys pushed off the bottom of it.

   ⚠️ **No check would have caught it.** Every selector the suite asserts was
   still somewhere in the file; only the layout was gone. Found by rendering the
   page, in seconds. Second time today a slice-based restore quietly dropped
   something — see the note in the spec about counting what you sliced. */
.tv {
  position: relative; display: flex; gap: 0;
  /* ⚠️ CAPPED BY THE WINDOW'S HEIGHT AS WELL AS ITS WIDTH, because the tube is
     4:3 and the set's width is what sets its height. At 860 the screen was
     536px tall on a 900px window. */
  margin: 0 auto; width: min(100%, 720px, 96vh);
  border-radius: 16px; padding: 18px;
  background: linear-gradient(162deg, #6E5233 0%, #4E3A25 42%, #3E2E1D 72%, #5A4229 100%);
  box-shadow: 0 20px 44px rgba(0, 0, 0, 0.6), 0 1px 0 rgba(255, 255, 255, 0.22) inset,
              0 -3px 0 rgba(0, 0, 0, 0.35) inset;
}
/* ⚠️ THE WOODGRAIN IS WHAT DATES IT. A plain brown box is a box; the grain is
   the whole reason it reads as a set from a front room. */
.tv::before {
  content: ''; position: absolute; inset: 0; border-radius: 16px; pointer-events: none;
  background: repeating-linear-gradient(94deg, rgba(0, 0, 0, 0.10) 0 1px, transparent 1px 4px,
    rgba(255, 255, 255, 0.035) 4px 5px, transparent 5px 9px);
}
/* ---- the cabinet's furniture ----
   Nick, 25 Sep 2026: *"make the tv more realistic."* None of these are things
   anybody names, and all of them are noticed missing: vents along the top, a
   moulded badge, feet, and a sheen on the glass. */
.tv-vents {
  position: absolute; left: 22%; right: 22%; top: 5px; height: 5px; border-radius: 3px;
  background: repeating-linear-gradient(90deg,
    rgba(0, 0, 0, 0.45) 0 2px, transparent 2px 6px);
  pointer-events: none;
}
/* ⚠️ HIS OWN NAME, moulded into the cabinet. A set has a maker's badge; putting
   a real manufacturer's there would be somebody else's trade mark on a page
   about Fergus. */
.tv-badge {
  position: absolute; left: 22px; bottom: 5px;
  font-family: var(--display); font-weight: 700; font-size: 8px; letter-spacing: 0.26em;
  text-transform: uppercase; color: rgba(255, 255, 255, 0.18);
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.55); pointer-events: none;
}
.tv-foot {
  position: absolute; bottom: -9px; width: 46px; height: 10px; border-radius: 0 0 4px 4px;
  background: linear-gradient(180deg, #33261A, #1E1610);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); pointer-events: none;
}
.tv-foot.l { left: 14%; } .tv-foot.r { right: 14%; }

.bezel {
  flex: 1 1 auto; min-width: 0; border-radius: 12px; padding: 16px;
  background: linear-gradient(180deg, #2B2622, #191512);
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.6) inset, 0 1px 0 rgba(255, 255, 255, 0.14);
}
.tube {
  position: relative; aspect-ratio: 4 / 3; overflow: hidden;
  border-radius: 26px/18px;              /* ⚠️ a CRT is not a rectangle */
  /* ⚠️ THE TUBE BULGES, from 26 Sep 2026. A dark screen is not one flat black:
     a CRT is brighter at the centre and falls away at the corners, because the
     glass is curved and the phosphor behind it is lit from a single gun. A flat
     `#070605` reads as a hole cut in the cabinet. Nick chose the middle of
     three treatments for the set. */
  background: radial-gradient(ellipse 118% 118% at 50% 45%,
              #1E1B18 0%, #0A0908 70%, #050403 100%);
  box-shadow: 0 0 0 2px #100D0B, 0 0 30px rgba(0, 0, 0, 0.92) inset;
}
/* ⚠️ NO SCANLINES ADDED HERE, AND THAT IS THE NOTE. The mockup proposed them
   and `.tube::after` further down ALREADY DRAWS THEM, together with a vignette.
   A second `.tube::after` would have been overridden by the later one anyway —
   dead CSS that looked like a feature. Checked before writing, not after.
   The only real change to the tube is the bulged base colour above. */
.tube iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* ⚠️ AN ARMED PLAYER IS INVISIBLE UNTIL THE SET IS ON, from 26 Sep 2026. The
   television builds its player before a channel is pressed now, and without
   this the frame sits on the screen where "NO SIGNAL. CHOOSE A CHANNEL" should
   be — in the first render it showed YouTube's own "Video player configuration
   error" to anybody who opened the page.

   ⚠️ `opacity`, NOT `visibility` OR `display`. The whole reason the frame is
   there early is to be a player a phone will let start; a browser will not
   keep one ready that it has been told not to render. Transparent, it is still
   being shown as far as the media rules are concerned. */
.tube:not(.on) iframe { opacity: 0; pointer-events: none; }
/* ⚠️ THE SHEEN ON THE GLASS, which is most of what says "screen" rather than
   "black rectangle" — a CRT's face is curved and always catches the room. It
   sits ABOVE the picture, so `pointer-events: none` is not optional here. */
.tube::before {
  content: ''; position: absolute; inset: 0; z-index: 5; pointer-events: none;
  border-radius: 26px/18px;
  background: linear-gradient(126deg,
    rgba(255, 255, 255, 0.09) 0%, rgba(255, 255, 255, 0.04) 18%,
    transparent 38%, transparent 100%);
}
/* the glass: scanlines, then the curve darkening the corners */
.tube::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 4;
  background:
    repeating-linear-gradient(180deg, rgba(0, 0, 0, 0.20) 0 1px, transparent 1px 3px),
    radial-gradient(130% 120% at 50% 45%, transparent 52%, rgba(0, 0, 0, 0.55) 100%);
}
/* ⚠️ `pointer-events: none`, AND IT IS THE WHOLE OF A REAL BUG. Nick, 25 Sep
   2026: *"once the videos are opened and playing there are no controls — it's
   impossible to stop them."*

   YouTube's own controls were there the entire time. This canvas covers the
   tube edge to edge, and **`opacity: 0` does not stop an element receiving
   clicks** — so every press meant for pause, scrub, volume or fullscreen
   landed on an invisible sheet of snow instead. The set looked finished and
   the player could not be touched.

   ⚠️ The same trap sits under `.tube::after` and `.tv::before`, which is why
   both of those have carried this line from the start. This one did not, and
   nothing in 676 checks could see it — a click that lands on the wrong
   element is not a wrong value anywhere. */
.snow {
  position: absolute; inset: 0; width: 100%; height: 100%; z-index: 3;
  pointer-events: none;
  image-rendering: pixelated; opacity: 0; transition: opacity 0.18s ease;
}
/* ⚠️ 0.82, NOT 1 — see the note further down, which is where this was
   accidentally set twice. At full strength the burst flares against a dark
   page, and it only lasts a third of a second. */
.tube.static .snow { opacity: 0.82; }
.tube-off {
  position: absolute; inset: 0; z-index: 2; display: grid; place-items: center;
  font-family: var(--display); font-weight: 700; font-size: 11px; letter-spacing: 0.3em;
  color: rgba(242, 227, 199, 0.3);
}
.tube.on .tube-off { display: none; }
/* ⚠️ Belt and braces on the same fault: `display: none` already stops this
   taking clicks, but it is one edit away from becoming `opacity: 0`. */
.tube-off { pointer-events: none; }

/* ⚠️ THE TAP PROMPT MUST NOT CATCH THE TAP. Its entire job is to send the next
   touch through to YouTube's own play button, which is the one gesture iOS
   will accept for a cross-origin video with sound. `pointer-events: none` is
   the feature, not a nicety — with it removed this element becomes the bug it
   was added to explain. Shown only when the frame has reported it is NOT
   playing; see tvWatch(). */
.tube-tap {
  position: absolute; left: 0; right: 0; bottom: 12%;
  margin: 0; text-align: center; pointer-events: none; z-index: 3;
  font: 600 11px/1.4 var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  letter-spacing: .22em; color: #f3e7cc;
  text-shadow: 0 1px 6px rgba(0,0,0,.85), 0 0 18px rgba(0,0,0,.6);
  animation: fadein .5s ease both;
}
.tube-tap[hidden] { display: none; }

/* ---- the controls down the right ----

   ⚠️ THE SCREEN SETS THE HEIGHT OF THE CABINET, AND THE KEYS FILL WHAT IS
   LEFT — 30 Sep 2026, and this is the reversal worth understanding.

   Nick: *"the buttons on the videos page make the TV screen longer than the
   YouTube video is... redesign to maximise the use of vertical space, so the
   side of the TV is just enough to contain the YouTube video."*

   **It was the wrong way round.** `.tv` is a flex row of two things — the bezel
   and this column — and a flex row is as tall as its tallest child. The tube is
   4:3, so its height follows from the cabinet's width and nothing else: 363px.
   The bezel around it came to 395. This column, with five keys of three lines
   each, came to **475** — so the column won, the bezel was stretched to match
   it, and 112px of the screen was black plastic under the picture. Measured
   before touching anything, in a browser, at 1280x900.

   **The fix is `flex: 1 1 0` on the key**, which is the whole of it. A flex
   basis of zero means a key contributes almost nothing to the column's natural
   height, so the column can no longer be the tallest child; the bezel is, the
   row is as tall as the screen, and the column is stretched to that. Then the
   keys grow to share it out.

   ⚠️ IT IS ALSO WHAT MAKES THIS SURVIVE A FIFTH CHANNEL. Fergus can add one
   from the admin — `syncChannels()` reads them from a table. Under the old
   arrangement every channel he added made the cabinet taller and the dead
   plastic deeper. Now another key takes its share of the same height and the
   set does not move. That is the reason to prefer this over simply making the
   keys shorter, which would have measured the same today and drifted back.

   ⚠️ `min-height` IS THE FLOOR, NOT THE SIZE. It is what stops the text being
   crushed if the channels ever outgrow the screen — at which point the column
   becomes the tallest child again and the cabinet grows, which is the right
   failure: a taller set beats a key you cannot read. On four channels it is
   nowhere near it. */
/* ⚠️ `min-width: 0` IS WHY THE COLUMN STAYS 152px, and without it the whole
   fix leaks sideways instead of downwards — 30 Sep 2026, found by rendering a
   channel called "Percussiveinstrumentation".

   `flex: 0 0 152px` reads like an instruction that cannot be disobeyed, and it
   is not: a flex item's automatic minimum size is its MIN-CONTENT size, which
   for this column is the longest word on any key. A long channel name pushed
   the column to 219px, and because the bezel takes what is left, **the screen
   shrank from 484 to 417 to make room for a button.** That is the same fault
   as the height one in the other axis — the controls sizing the picture.

   ⚠️ IT WAS ALREADY TRUE BEFORE THE KEYS WERE CHANGED, measured at 192px with
   the old three-line keys, so this is not a regression being patched — it is a
   fault the one-line key made slightly worse (the number now shares the line,
   so the min-content width is wider) and therefore visible.

   Nick, 30 Sep 2026: *"wrap the word if needed."* This, with `min-width: 0`
   and `overflow-wrap` on `.lab`, is what makes it wrap rather than push. */
.tv-panel {
  flex: 0 0 152px; min-width: 0;
  margin-left: 16px; display: flex; flex-direction: column; gap: 9px;
}
.chan {
  border: 0; cursor: pointer; text-align: left; padding: 9px 10px; border-radius: 4px;
  /* ⚠️ ONE LINE: THE NUMBER, THEN THE NAME. Nick, 30 Sep 2026: *"we want to
     keep the number and the title... put the title on the same line as the
     channel number and wrap the word if needed."* */
  display: flex; align-items: center; gap: 8px;
  flex: 1 1 0; min-height: 38px;
  font-family: var(--display); font-weight: 700; font-size: 9px; letter-spacing: 0.13em;
  text-transform: uppercase; line-height: 1.3;
  color: #3A3129;
  background: linear-gradient(180deg, #D8D2C6, #B3ACA0 70%, #C6C0B4);
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.4), 0 1px 0 rgba(255, 255, 255, 0.55) inset;
  transition: transform 0.09s ease, box-shadow 0.09s ease;
}
/* ⚠️ `min-width: 0` IS LOAD-BEARING, not tidiness. A flex item will not
   shrink below the width of its longest word unless it is told it may, so
   "Amazing Movies" on a 152px key would otherwise push the key wider than the
   column instead of wrapping onto a second line. `overflow-wrap` is the second
   half: it lets a single long word break rather than stick out. */
.chan .lab { flex: 1 1 auto; min-width: 0; overflow-wrap: break-word; }
/* ⚠️ THE FIFTH KEY IS AN `<a>`, so it needs the two things a `<button>` gave it
   for free — the box model and no underline. It is a link on purpose: it leaves
   the site, and a browser will only open a new tab, offer a long press and
   announce "link" for something that really is one. Everything else about it is
   `.chan`, because it has to read as the same piece of hardware. */
.chan-out {
  /* ⚠️ `flex`, NOT `block`, from 30 Sep 2026 — it has to be the same one-line
     object as the channel keys now, or the fifth key is the only three-line
     thing left on the cabinet. `.chan` above supplies the rest. */
  text-decoration: none;
  /* it never latches — it is not a channel you are "on" */
}
.chan-out:hover { text-decoration: none; }

.chan:active,
.chan[aria-current="true"] {
  transform: translateY(3px);
  box-shadow: 0 0 0 rgba(0, 0, 0, 0.4), 0 2px 4px rgba(0, 0, 0, 0.4) inset;
}
.chan[aria-current="true"] { background: linear-gradient(180deg, #E8A47C, #D9541F 72%, #C1481A); color: #2A1E14; }
/* ⚠️ NO LONGER `display: block` — 30 Sep 2026. It was the key's first LINE;
   it is now the first thing on the key's ONLY line, so it sits inline beside
   the name and is stopped from shrinking. */
.chan .n { flex: 0 0 auto; font-size: 14px; letter-spacing: 0.06em; }
/* ⚠️ `.how-many` HAS BEEN DEAD SINCE THE COUNT MOVED INTO `.cnt`, and its
   rules are deleted rather than kept, 30 Sep 2026. Nothing has written this
   class for weeks; a styled class nobody emits is the "dead CSS that looks
   like a feature" this file warns about a few rules up. The name is left in
   this note so a session searching for it finds the answer instead of
   nothing. */
/* ⚠️ A LINK, not a button, because it leaves the site — see the note in
   `videos()`. It is styled as a key so the set reads as having three of them. */
.chan-all {
  display: block; text-decoration: none;
  background: linear-gradient(180deg, #C9C3B7, #A49D91 70%, #B7B1A5);
}
.chan-all .n { font-size: 11px; }
.chan-all:hover { background: linear-gradient(180deg, #D8D2C6, #B3ACA0 70%, #C6C0B4); }

/* ⚠️ THE BOTTOM ROW IS PINNED TO THE FLOOR OF THE COLUMN. `margin-top: auto`
   used to be on NEXT; it moved here when the power knob joined it, or the two
   would have sat apart with the gap between them. */
/* ⚠️ THE CONTROLS ARE AT THE TOP NOW, from 26 Sep 2026 — Nick: *"I want on
   and next to be top, prominent on the tv ctrls so pushing on then next is the
   natural step for user."* They were under five channel keys, so the panel
   read channels-first and the two things you need before any of them were
   last. `.tv-bottom` is gone with them. */
/* ⚠️ `flex: 0 0 auto` FROM 30 Sep 2026. The channel keys are `flex: 1 1 0`
   and grow to fill the column; without this the row of controls would grow
   and shrink with them, and ON/NEXT are a fixed piece of hardware. */
.tv-top { display: flex; gap: 8px; align-items: stretch; margin-bottom: 2px; flex: 0 0 auto; }

/* ⚠️ THE OFF SWITCH. Nick could not stop a video — the real fault was the snow
   canvas eating the clicks, and this is the other half: a set has a way to turn
   it off, and reaching for the page's own control rather than the player's is
   what a person actually does. */
/* ⚠️ IT SAYS "ON" NOW. It was a 44px square with an unlabelled knob in it —
   authentic to a real set and useless to somebody who has never seen this one.
   The lamp stays, because the lamp is what says whether it is on; the word is
   what says what it does. */
/* ⚠️ MOULDED, NOT PRINTED — 26 Sep 2026. Nick: *"Can the buttons be made a bit
   more tv set like."* They were dark rectangles with a word on them, which is
   a web button painted brown.

   Three things make a key read as hardware, and all three are cheap:
   a **bevel** (light along the top edge, shadow along the bottom), a **face
   that is not flat** (a gradient that turns over about a third of the way
   down, the way moulded plastic catches a room light), and a **travel** — the
   key moves DOWN when pressed and its shadow disappears, because a key that
   only changes colour reads as a link.

   ⚠️ THE FAMILY IS THE CHANNEL KEYS. These are the same object in a darker
   plastic — same radius, same bevel, same 4px of travel. Two different button
   languages on one cabinet is what made the old ones look printed on. */
.tv-power,
.tv-next {
  border: 0; cursor: pointer; border-radius: 5px;
  padding: 11px 14px; position: relative;
  font-family: var(--display); font-weight: 700;
  font-size: 11px; letter-spacing: 0.22em; color: #F2E3C7;
  background:
    linear-gradient(180deg, #5A534B 0 2px, #3A342E 2px 38%, #211D19 39%, #15120F 88%, #2A2622);
  box-shadow:
    0 4px 0 #100D0B,                          /* the key's own depth */
    0 5px 6px rgba(0, 0, 0, 0.55),            /* its shadow on the cabinet */
    0 1px 0 rgba(255, 255, 255, 0.22) inset,  /* the lit top edge */
    0 -1px 0 rgba(0, 0, 0, 0.6) inset;        /* the dark under-edge */
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.8);
}
.tv-power {
  flex: 0 0 auto;
  display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 8px;
}
/* ⚠️ PRESSED IS DOWN AND UNDER-LIT, not merely darker. The 4px of travel is
   the same figure the channel keys use; the shadow going with it is what makes
   the key look like it has actually moved rather than changed colour. */
.tv-power:active,
.tv-next:active {
  transform: translateY(4px);
  box-shadow: 0 0 0 #100D0B, 0 1px 2px rgba(0, 0, 0, 0.5),
              0 1px 0 rgba(255, 255, 255, 0.1) inset;
}

.tv-power .pw {
  font-family: var(--display); font-weight: 700;
  font-size: 11px; letter-spacing: 0.22em; color: #F2E3C7;
}
/* The knob is kept for the record of what was there; nothing draws it now. */
.tv-power .knob {
  display: none;
  width: 20px; height: 20px; border-radius: 50%;
  background: radial-gradient(circle at 36% 30%, #E8E3D8, #9A9288 52%, #4A443E 88%);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(0, 0, 0, 0.5);
}
/* ⚠️ Dark when off, not hidden. A lamp that disappears is a hole in the wood;
   an unlit lamp is a lamp. Same rule as the record player's. */
.tv-power .lamp {
  width: 6px; height: 6px; border-radius: 50%;
  background: #4A1A0E; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6) inset;
  transition: background 0.2s ease, box-shadow 0.2s ease;
}
.tv-power[aria-pressed="true"] .lamp {
  background: #FF6A3D; box-shadow: 0 0 7px rgba(255, 106, 61, 0.85);
}

.tv-next { flex: 1 1 auto; }
/* ⚠️ `flex: 0 0 auto` alongside the height, 30 Sep 2026 — a flex item with
   a height is still allowed to SHRINK below it by default, and with the keys
   now growing to fill the column the speaker was the thing that gave way. */
.tv-grille {
  height: 38px; flex: 0 0 auto; border-radius: 4px;
  background: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0.45) 0 1.1px, transparent 1.2px) 0 0/6px 6px,
    linear-gradient(180deg, #5C4630, #43331F);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.18) inset;
}
.readout {
  font-family: var(--hand); font-size: 12.5px; color: var(--flame);
  margin: 10px 2px 0; min-height: 34px; text-align: center;
}

/* ⚠️ THE STRIP OF STILLS IS GONE, 25 Sep 2026. Nick: *"remove the thumbnails
   at the bottom of the tv and instead have a 'watch all in youtube' as a third
   button on the tv."*

   ⚠️ **This reverses his own choice of an hour earlier**, and it costs what
   that choice was for: with the strip gone there is no way to see what is on
   the set or to jump to one video — it is channel, NEXT, or leave for YouTube.
   He made the trade knowingly. This note is here so nobody quietly puts it
   back as an improvement. */

/* ⚠️ THE SET STACKS BELOW 620px AND THE SPEAKER GOES. Turned sideways, the
   controls are a row under the screen — which is what a portable set did — and
   a speaker grille in a row of buttons reads as a sixth button. */
@media (max-width: 620px) {
  /* ⚠️ THE WHOLE SET HAS TO FIT ONE PHONE SCREEN, from 26 Sep 2026. Nick:
     *"it's too tall I would like it all on one screen on phone."*

     Measured before changing anything: the set ended at y=678 and a phone
     shows about 660 once the browser's own chrome is taken off — so it was
     over by a little, and every fix below is small on purpose. Making the tube
     smaller would have been the easy 60px and the wrong one: the screen is
     what the page is for.

     What went instead is padding and the second line on each channel key. */
  .tv { flex-direction: column; padding: 9px; }
  .tv-panel { flex: 0 0 auto; margin: 9px 0 0; flex-direction: row; flex-wrap: wrap; gap: 6px; }

  /* ⚠️ ON AND NEXT GET THE WHOLE ROW. As a flex item among the channel keys
     they were two small squares beside a wide one, which is the opposite of
     "prominent" — and the point of moving them was that they are the first two
     things anybody presses. */
  .tv-top { flex: 1 1 100%; margin: 0; }
  .tv-power, .tv-next { padding: 10px 14px; }

  /* ⚠️ `flex: 1 1 40%` OVERRIDES THE `flex: 1 1 0` THE KEY NOW CARRIES, and
     it has to. Sideways the panel is a WRAPPING ROW, so the flex basis is a
     WIDTH here, not a height — 40% is what puts two keys on a line. A basis of
     zero would pack all four onto one row, each about 24px wide. */
  .chan { flex: 1 1 40%; padding: 7px 9px; line-height: 1.25; min-height: 0; }
  .chan .n { font-size: 12px; }

  /* ⚠️ THE VIDEO COUNT AND "NEXT" WENT FROM EVERY SIZE ON 30 Sep 2026, so
     the phone no longer has to hide them. This block used to carry
     `.chan .of { display: none }` and three rules trimming the YouTube key
     from three lines to one — all of them now describe a shape that does not
     exist at any width. **The desktop key IS the phone key now:** a number and
     a name on one line.

     The reason it was cut on the phone first is still worth keeping: the whole
     set has to fit one screen, and a second line on every key cost about 14px
     a row. Nick's 30 Sep change did the same thing everywhere and for the same
     kind of reason — height. */

  /* ⚠️ THE YOUTUBE KEY KEEPS ITS NUMBER NOW. It used to be hidden here to buy
     a line; the key is one line at every width, so hiding it would only make
     the fifth key the odd one out in a row of numbered keys. It cannot be
     REMOVED — Nick had "Watch all on YouTube" taken off the readout on 26 Sep
     and it became this key, so it is the only way out to the channel. */
  .chan-out { padding: 7px 9px; }

  /* The last few pixels, taken from the gaps rather than from anything that
     has to be read. Measured: 679 before this block, 637 after. */
  .tv { padding: 7px; }
  .tv-panel { gap: 5px; margin-top: 7px; }
  .chan { padding: 6px 9px; }
  /* ⚠️ EACH OF THESE GETS ITS OWN ROW, or the wrap packs them alongside the
     two channel keys — which put the power knob and NEXT into the gap beside
     "Watch all on YouTube", and pushed the whole block down over the badge
     moulded into the cabinet. Found by rendering at 390px, not by a check. */
  .chan-all { flex: 1 1 100%; }
  .tv-bottom { flex: 1 1 100%; margin-top: 0; }
  .tv-next { margin: 0; flex: 1 1 auto; text-align: center; }
  .tv-grille { display: none; }
  /* The cabinet is too small to carry a badge as well as three keys and a
     knob. The vents and the feet stay — they cost no room. */
  .tv-badge { display: none; }
}

/* ⚠️ THE FACE OF THE KEY IS NOW A NUMBER AND A NAME, AND NOTHING ELSE —
   30 Sep 2026. `.chan .cnt`, `.chan .of`, `.chan .nx` and `.nx-lamp` are gone
   from the stylesheet with the markup that carried them, because nothing emits
   those classes any more and a styled class nobody writes is the dead CSS this
   file warns about twice.

   **What they were, so nobody re-derives it from an empty space:**

   • `.cnt` was the second line — a flex row holding the count and "Next".
   • `.of` was "3 videos", and `mark()` in `app.js` rewrote it to "1 of 3" on
     whichever channel was live.
   • `.nx` was "NEXT ›" on the face of every key. Nick asked for it on 25 Sep
     2026 because pressing the channel you are already on advances to its next
     video, and the key's face was the only place that was written down.
   • `.nx-lamp` was the small lamp beside that word, added the same day, lit on
     the channel you were watching. It lit CREAM rather than orange, because by
     then the key itself had gone orange and an orange lamp on an orange key is
     invisible — the only pale lamp on the site.

   **Nothing they said has actually been lost**, which is why they could go:
   the NEXT key at the top of the panel says "next" once instead of five times,
   the key going orange says which channel is live more loudly than a 6px lamp
   did, and the readout under the set says `Live Originals · 1 of 3 — title`
   from the moment anything plays. What HAS gone is the count on a channel you
   have not pressed — you no longer learn there are three videos on Drums until
   you tune to it. Nick's call, 30 Sep 2026, and the price of the height. */

/* ⚠️ THE SNOW IS A TRANSITION AGAIN, NOT A RESTING STATE — and getting that
   wrong is a bug I shipped. Nick: *"an issue with the TV: the snow doesn't go
   away, so it covers the video."*

   When the set had no cabinet the snow ran ALL THE TIME at 55%, which is what
   made a dark rectangle read as a screen with no signal. Bringing the cabinet
   back, I kept that line — and `.snow { opacity: 0.55 }` sits AFTER the
   cabinet's own `opacity: 0`, so it won, and 55% snow sat permanently over
   every video.

   ⚠️ **The dimming was right for the panel it was written for and wrong for
   this one.** A rule carried across a redesign because it was good is exactly
   how this happens. What survives is the VALUE RANGE — 30–235 rather than
   0–255, which stops it strobing — because that is true of snow either way.

   ⚠️ AND THE RULE LIVES IN ONE PLACE. Writing the fix here left
   `.tube.static .snow` declared twice — the exact shape of the bug it was
   fixing. It is set once, up with `.snow` itself. */

.after { display: flex; gap: 22px; align-items: baseline; margin: 14px 0 0; flex-wrap: wrap; }
.plain {
  border: 0; background: none; padding: 0; cursor: pointer; color: var(--dim);
  font-family: var(--hand); font-size: 17px; text-decoration: none;
}
.plain:hover { color: var(--flame); }

/* ---- home: the ask, and two ways out of it --------------------------------
 *
 * Nick, 25 Sep 2026: *"change the homepage so that it just starts in the middle
 * with the writing… and then 'Sign up', and then it's got one arrow to the left
 * that says 'Live' and a [right] arrow that says 'Discography'."*
 *
 * ⚠️ THE SIX DOORS ARE GONE. Every one of them is in the index across the top
 * of every page, which is where the navigation moved this morning — so they
 * were the same list a second time, sitting on top of the thing this page
 * actually asks for.
 * -------------------------------------------------------------------------- */

.home-ask { display: flex; flex-direction: column; justify-content: center; }

/* ⚠️ THREE COLUMNS: an arrow, the ask, an arrow. Nick, 25 Sep 2026: *"on the
   right of that block, have the discography point to the right. On the left,
   have 'Live' point to the left."* The page reads outwards from the thing it
   is asking for. */
.home-row {
  display: flex; align-items: center; justify-content: center;
  gap: clamp(16px, 4vw, 54px);
}
.home-ask-col { flex: 0 1 auto; min-width: 0; text-align: center; }
/* Everything in the ask centres with it — the lines, and the button, which has
   `margin: auto` rather than an alignment because it is an inline-block. */
.home-ask-col .ask-words { text-align: center; }
.home-ask-col .ask-form { justify-content: center; }
.home-ask-col .ask-go { margin-left: auto; margin-right: auto; }

/* ⚠️ FOUR ARROWS, TWO A SIDE, from 25 Sep 2026. Nick: *"Mixtapes under
   Discography with the arrow. Use the same typeface as Discography has and the
   same kind of arrow. Video: put that under Live, same thing."*

   ⚠️ **This reverses the note that used to sit here** — that an arrow is a
   direction and a list is a list, and five arrows would be a compass rose.
   Four is not five, they are in two columns rather than a row, and he has
   looked at both. The old reasoning is deleted rather than left arguing with
   the page. */
.home-side {
  display: flex; flex-direction: column; gap: clamp(8px, 1.6vh, 16px);
  flex: 0 0 auto;
}
/* Each side's words line up with each other: the left column's against the
   arrows on their right, the right column's against theirs on the left. */
.home-side-left { align-items: flex-end; }
.home-side-right { align-items: flex-start; }

/* ⚠️ THE ARROWS GO UNDER THE ASK ON A PHONE, not beside it. At 390px a
   three-column row leaves the writing about 150px wide, which is four words a
   line. Below it they still read left and right, which is the whole idea. */
@media (max-width: 760px) {
  .home-row { flex-wrap: wrap; align-items: flex-start; }
  .home-ask-col { flex: 1 1 100%; order: -1; margin-bottom: clamp(14px, 3vh, 28px); }
  .home-side { flex: 1 1 42%; }
  /* At this width the two columns are already narrow; centring the words
     inside them stops the pairs drifting apart from each other. */
  .home-side-left { align-items: center; }
  .home-side-right { align-items: center; }
}
/* ⚠️ THE ASK AND THE ARROWS ARE CENTRED AS ONE BLOCK. Putting the auto margin
   on the ask alone centred the ask and pushed the arrows to the floor of the
   panel, half a screen below the thing they belong to.

   ⚠️ `margin-block: auto`, NOT `justify-content: center` alone. A centred flex
   column taller than its box overflows at BOTH ends, and the top overflow
   cannot be scrolled to — the same trap `.pane` documents further up, which put
   six buttons above the top edge of the page on a short window. */
.home-mid { margin-block: auto; width: 100%; }

/* ⚠️ `.ways` IS GONE with the row it laid out — the two arrows sit either side
   of the ask now rather than under it, so there is no row of their own. */
.way {
  display: inline-flex; align-items: center; gap: 10px;
  color: var(--ink); text-decoration: none;
}
/* The word sits on the side the arrow points away from, so the pair reads
   outwards in the direction it is sending you. */
.way-right { flex-direction: row-reverse; }
.way-arrow { width: clamp(44px, 6vw, 62px); height: auto; overflow: visible; color: var(--flame); }
.way-word {
  font-family: var(--hand);
  font-size: clamp(20px, min(2.6vw, 4vh), 32px); line-height: 1.1;
}
.way:hover .way-word { color: var(--flame); }
.way:focus-visible { outline: 2px solid var(--flame); outline-offset: 4px; }

@media (max-width: 620px) {
  /* ⚠️ STILL A ROW, not a column. Two arrows pointing left and right stacked
     vertically stop being a pair and become a list with decorations. */
  .ways { gap: 18px; width: 100%; justify-content: space-between; }
}

/* ---- Follow / Contact: drawn buttons, not a numbered list -----------------
 *
 * Nick, 25 Sep 2026: *"in the footer, let's make this look a little bit more
 * like the rest of the design… instead of just having those lists like that,
 * have a drawn button with Spotify written in the middle of it, a drawn button
 * with TikTok, one with YouTube, and one with email."*
 *
 * ⚠️ THE NUMBERED LIST WAS THE ODD ONE OUT. Every other control on this site is
 * a word with a hand-drawn line round it; this panel alone was an ordered list
 * with rules between the rows — which is the pattern the GIG list uses, and a
 * gig list is a list. Four places to follow somebody is a row of buttons.
 * -------------------------------------------------------------------------- */

.social-row {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: clamp(10px, 2vw, 20px);
  margin: clamp(10px, 2vh, 18px) 0 0;
}
/* ⚠️ IT REUSES `.door`, which is what `fitDoors()` measures — the box is drawn
   at the width the handwriting turned out to be, and a second class would have
   to be added to that list or the boxes come out wonky. That is the fault the
   doors had on 21 Sep, and it is why this is a `.door` with a modifier rather
   than a button of its own. */
.social-door { flex: 0 0 auto; }
/* ⚠️ A SECOND HEADING NEEDS AIR ABOVE IT. "Contact me" landed hard against the
   row of buttons — a heading that touches the thing above it reads as a caption
   for it rather than as the start of something new. */
.social-row + .head-row { margin-top: clamp(22px, 4.5vh, 48px); }

@media (max-width: 620px) {
  /* Two across rather than a long row that wraps one at a time. */
  .social-door { flex: 1 1 42%; }
}

/* ---- the tracks and the note, folded away ---------------------------------
 *
 * Nick, 25 Sep 2026: *"a track list that should appear just as tracks, with a
 * very low-key way to just accordion it. Probably a triangle, and they can push
 * the triangle and it'll accordion down the tracks… and if there's a cover
 * note, have the cover note and have it pop out as well."*
 *
 * ⚠️ `<details>`, NOT A SCRIPT. The browser gives the open and shut, the
 * keyboard, the announcement to a screen reader, and finding text inside a
 * closed one — all of which a class-toggling handler would have had to be
 * written to do, and would have got wrong.
 *
 * ⚠️ AND IT IS WHAT MAKES ROOM FOR THE PLAYERS. The panel does not scroll, and
 * an open tracklist was the thing pushing the Spotify module off the bottom of
 * it.
 * -------------------------------------------------------------------------- */

/* ⚠️ NOT `--rec-sw` ANY MORE. A fold in the band sizes itself: shut it is its
   own summary's width, open it is the whole card. The old rule pinned it to the
   sleeve's width, which is exactly the constraint the two players needed lifted
   to sit side by side. */
.fold { max-width: 100%; }
.fold + .fold { margin-top: 4px; }
.fold > summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: 8px;
  font-family: var(--display); font-weight: 700;
  font-size: 9.5px; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--dim);
  padding: 5px 0;
}
/* Safari puts its own disclosure triangle in unless this is said as well. */
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary:hover { color: var(--ink); }
.fold > summary:focus-visible { outline: 2px solid var(--flame); outline-offset: 3px; }

/* ⚠️ THE TRIANGLE IS DRAWN HERE because the browser's own marker is a different
   shape, size and colour on every platform — and this one has to be small
   enough to read as "low-key", which was the whole instruction. */
.fold-tri {
  width: 0; height: 0; flex: 0 0 auto;
  border-left: 6px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 0.18s ease;
}
.fold[open] > summary .fold-tri { transform: rotate(90deg); }

/* ⚠️ ALIGNED LEFT, as asked, and the numbers no longer carry a "Tracks:" cue —
   the word is on the flap above them. */
.fold .rec-tracks { margin: 2px 0 8px; text-align: left; }
.fold .rec-about { margin: 2px 0 8px; }


/* ---- the two players, side by side where there is room ---------------------
 *
 * Nick, 26 Sep 2026, on what the rearrangement was FOR: *"allows, when we hit
 * Listen, for the Spotify widget and the Watch Everywhere widget to appear side
 * by side."*
 *
 * ⚠️ NO MARKUP CHANGED. `odesliFrame()` emits four flat siblings — cue, slot,
 * cue, frame — and this places them into two columns. Rewriting it to wrap each
 * pair in a div would have been the obvious move and would have broken
 * `spotifyMount()`, which replaces `.rec-spot` in place.
 *
 * ⚠️ `:has()` GUARDS IT. With no Spotify link `odesliFrame` emits only TWO
 * children, and without this guard they would take column one and leave column
 * two empty — a player sitting in half the space for no reason.
 *
 * ⚠️ AND IT STOPS AT 640px, WHICH IS THE WHOLE POINT OF THE BREAKPOINT. Two
 * columns on a 390px phone gives each about 160px, and Spotify's compact player
 * stops showing the track name and the scrubber at that width — it becomes a
 * play button in a box. Measured by building it and looking, not assumed.
 * Nick, shown both: *"having them stacked on top of each other at phone width
 * is absolutely fine."* */
@media (min-width: 641px) {
  .odesli-in:has(> :nth-child(4)) {
    /* ⚠️ THE 420px CAP COMES OFF HERE, and only here. `.odesli` is capped at
       420px for the STACKED layout, where a single player wider than that
       looks stranded in a card. Side by side that cap is shared between two
       columns — it left each one 198px, which is the same cramped player the
       phone breakpoint exists to avoid. Measured in the real page, not
       assumed: the card is 600px and the columns were 198px. */
    max-width: none;
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: auto auto;
    column-gap: clamp(14px, 2vw, 24px);
    align-items: start;
  }
  .odesli-in:has(> :nth-child(4)) > :nth-child(1) { grid-area: 1 / 1; }
  .odesli-in:has(> :nth-child(4)) > :nth-child(2) { grid-area: 2 / 1; }
  .odesli-in:has(> :nth-child(4)) > :nth-child(3) { grid-area: 1 / 2; }
  .odesli-in:has(> :nth-child(4)) > :nth-child(4) { grid-area: 2 / 2; }
  /* ⚠️ The stacked rule put a top margin on the second cue to separate it from
     the player above. Side by side there is nothing above it, and the margin
     pushed it out of line with the first one. */
  .odesli-in:has(> :nth-child(4)) .listen-cue ~ .listen-cue { margin-top: 0; }
}

/* ---- the cover note, printed on the sleeve --------------------------------
 *
 * ⚠️ ON THE ARTWORK, from 26 Sep 2026. Two readings of *"the bottom left of
 * each record label, in the bottom-left corner"* were drawn — under the band,
 * and on the sleeve — and Nick picked the sleeve knowing the cost: *"It's okay
 * that it covers a little bit of the artwork. That's fine."*
 *
 * ⚠️ THE WASH IS NOT DECORATION. Fergus chooses the artwork and we do not, so
 * the note has to stay legible over a photograph nobody has seen yet. */
.rec-liner {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 4;
  padding: clamp(24px, 6%, 40px) clamp(10px, 3%, 16px) clamp(8px, 2.4%, 13px);
  background: linear-gradient(to top,
              rgba(0,0,0,.88) 0%, rgba(0,0,0,.72) 40%,
              rgba(0,0,0,.34) 72%, transparent 100%);
}
/* ---- the tracklist, printed on the sleeve ---------------------------------
 *
 * ⚠️ TOP-LEFT, OVER THE ARTWORK, from 26 Sep 2026. Nick: *"Move 'tracks'
 * accordion to the top left of the album cover so the tracks if they are there
 * deploy over the album artwork."*
 *
 * ⚠️ WHY THIS IS BETTER THAN THE BAND, and it is not only taste: in the band an
 * open tracklist made the whole card taller and `fitRecordCard()` shrank the
 * record to pay for it — so reading the songs cost you the thing you were
 * looking at. Over the sleeve it costs nothing. The card is exactly the same
 * height open or shut.
 *
 * ⚠️ IT MIRRORS THE LINER NOTE rather than inventing a second treatment: the
 * same wash, upside down. The note has to stay legible over an artwork nobody
 * has seen yet, and so does this. */
/* ⚠️ CAPPED AT 52%, AND THIS IS WHY. The tracklist is anchored to the top of
   the sleeve and the cover note to the bottom, and they share one surface. On a
   desktop the sleeve is ~340px and either can have what it likes; at a TRUE
   390px phone viewport it is ~170px, and with both open the songs ran straight
   through the note — each perfectly legible on its own and illegible together.

   ⚠️ Found by rendering at a real 390px viewport, which needed an iframe:
   headless Chrome clamps its own window to 500px wide, so every "phone"
   screenshot before this one was a 500px screenshot scaled down, and the
   collision never appeared in any of them.

   46% each leaves both clear of the other at any sleeve size — with 8% of the
   sleeve between them — no JavaScript and nothing to keep in step. Either panel
   scrolls inside its own share rather than growing into the other's.
   ⚠️ 52% each was the first attempt and overlapped by 7px, because 52 + 52 is
   104. The caps were resolving perfectly; the arithmetic was wrong. */
.rec-tracks-fold {
  position: absolute; top: 0; left: 0; right: 0; z-index: 4;
  max-height: 46%; display: flex; flex-direction: column;
}
.rec-tracks-fold > summary {
  list-style: none; cursor: pointer; flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 7px; align-self: flex-start;
  padding: clamp(8px, 2.4%, 13px) clamp(10px, 3%, 16px);
  font-family: var(--display); font-weight: 700; font-size: 10px;
  letter-spacing: .18em; text-transform: uppercase;
  color: rgba(255, 255, 255, .88); text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
}
.rec-tracks-fold > summary::-webkit-details-marker { display: none; }
.rec-tracks-fold > summary:hover { color: #fff; }
/* ⚠️ THE WASH ONLY ARRIVES WITH THE LIST. Shut, this is two words on the
   artwork and a permanent dark band under them would be a smudge on every
   sleeve for a control nobody has pressed. */
.rec-tracks-fold[open] {
  background: linear-gradient(to bottom,
              rgba(0,0,0,.88) 0%, rgba(0,0,0,.72) 45%,
              rgba(0,0,0,.34) 78%, transparent 100%);
}
.rec-tracks-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: 0 clamp(10px, 3%, 16px) clamp(16px, 5%, 26px);
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent 100%);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent 100%);
}
/* the songs, white on the wash rather than the card's ink */
.rec-tracks-fold .rec-tracks { color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,.7); }

/* ⚠️ THE HEADING IS FOR SCREEN READERS ONLY. Nick: *"remove where it says cover
   notes because that should be obvious."* It is obvious from the hand font and
   the place it sits — and NEITHER of those is announced, so to somebody not
   looking at it the note would arrive as an unexplained sentence on a picture.
   Not `display:none`, which is not announced at all. */
.rec-liner-h {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.rec-liner-text {
  margin: 0; font-family: var(--hand);
  /* ⚠️ vw, NOT cqw. Container units need `container-type` on an ancestor and
     nothing here declares one, so `cqw` would fail silently and the note would
     inherit whatever size it landed next to. The rest of this card sizes on vw
     — see `.rec-kind` — so it matches rather than introducing a containment
     dependency for one line of text. */
  font-size: clamp(11px, 1.5vw, 17px); line-height: 1.36;
  color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,.6);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
.rec-liner-more {
  margin: 3px 0 0; padding: 0; background: none; border: 0; cursor: pointer;
  font-family: var(--display); font-weight: 800; font-size: 9px;
  letter-spacing: .2em; text-transform: uppercase; color: rgba(255,255,255,.82);
}
.rec-liner-more:hover { color: #fff; }
/* ⚠️ THE OPEN PANEL IS CAPPED BY THE SLEEVE, NOT A FRACTION OF IT. Capping the
   TEXT at a share of the sleeve's height sliced the last line in half
   horizontally — it read as broken rather than as "there is more". The panel
   takes up to all of the sleeve, only the text scrolls, the word stays pinned
   and reachable, and the mask fades a cut line so it reads as continuing. */
.rec-liner[data-open="yes"] {
  /* ⚠️ 46%, the other half of the pair above — the two panels share one sleeve
     and neither may grow into the other. 52% each was the first attempt and it
     OVERLAPPED BY 7px, because 52 + 52 is 104. Measured, not eyeballed: the
     caps were resolving correctly and the arithmetic was simply wrong. */
  max-height: 46%; display: flex; flex-direction: column; gap: 2px;
  padding-top: clamp(14px, 4%, 22px);
}
/* ⚠️ THE FADE NEEDS ROOM OF ITS OWN, fixed 26 Sep 2026. Nick, on a phone:
   *"the last line of the cover notes can't be seen... the mask covers it up."*

   The mask fades the bottom 18px so a cut line reads as continuing rather than
   as chopped. With nothing under the text, those 18px land on the LAST LINE —
   so a note that fits perfectly had its final line faded to nothing, which is
   the opposite of what the fade is for. The padding gives the fade something
   empty to work on.

   ⚠️ It is `padding-bottom` on the SCROLLING element, not a margin: a margin
   sits outside the scroll box and the mask would still land on the text. */
.rec-liner[data-open="yes"] .rec-liner-text {
  -webkit-line-clamp: unset; overflow-y: auto; flex: 1 1 auto; min-height: 0;
  padding-bottom: 20px;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent 100%);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent 100%);
}
.rec-liner[data-open="yes"] .rec-liner-more { flex: 0 0 auto; align-self: flex-start; }


/* ⚠️ UNDER THE PLAYER, NEVER OVER IT. This line appears when a phone refused
   to let the record start on its own, and the next tap has to reach Spotify's
   own play button — so it must not be in the way of it. Overlaying the player
   would make this message the reason the music never starts. */
.spot-tap {
  margin: 6px 0 0; padding: 0;
  font: italic 11.5px/1.35 Georgia, serif;
  color: #b9a88f; text-align: center;
  animation: fadein .4s ease both;
}

/* ---- the sound check ------------------------------------------------------
 *
 * An unlisted diagnostic page, for finding out what a phone is actually doing
 * with sound. Plain and large: it is read on a phone, in whatever light, by
 * somebody being asked to report what they heard.
 *
 * ⚠️ IT MUST STAY ABOVE THE HOME SECTION THAT FOLLOWS. Everything from there
 * to the end of this file is held to a rule that every selector is scoped to
 * the home page, and the suite finds that section by searching for its first
 * selector as TEXT — so a rule appended at the end of the stylesheet lands
 * inside it and fails, and a comment that so much as quotes that selector
 * moves the boundary. Both of those happened while this was being added.
 */
.sc { margin-top: 8px; }
.sc-step { margin: 0 0 26px; padding: 0 0 22px; border-bottom: 1px solid rgba(242,227,199,0.18); }
.sc-step:last-of-type { border-bottom: 0; }
.sc-step h3 { margin: 0 0 6px; font-family: var(--display); font-size: 17px; }
.sc-step p { margin: 0 0 12px; font-size: 14px; line-height: 1.5; color: var(--dim); }
.sc-go {
  display: inline-block; margin: 0 8px 8px 0; padding: 14px 20px;
  background: var(--flame); color: #fff; border: 0;
  font-family: var(--body); font-size: 15px; font-weight: 600; cursor: pointer;
}
.sc-ask { margin-top: 14px; }
.sc-ask p { margin: 0 0 8px; color: var(--paper); }
.sc-a {
  display: block; width: 100%; margin: 0 0 8px; padding: 13px 15px; text-align: left;
  background: transparent; color: var(--paper); border: 1px solid rgba(242,227,199,0.35);
  font-family: var(--body); font-size: 15px; cursor: pointer;
}
.sc-a.on { background: var(--paper); color: var(--ground); border-color: var(--paper); }
.sc-out { margin-top: 10px; padding: 16px; border: 1px solid rgba(242,227,199,0.28); }
.sc-out h3 { margin: 0 0 10px; font-family: var(--display); font-size: 15px; }
.sc-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.sc-table th {
  text-align: left; padding: 7px 10px 7px 0; vertical-align: top;
  font-weight: 600; color: var(--dim); width: 45%;
}
.sc-table td { padding: 7px 0; vertical-align: top; word-break: break-word; }
.sc-hint { margin: 12px 0 0; font-size: 13px; color: var(--dim); }


/* ==== HOME: THE SPLIT =====================================================
 *
 * ⚠️ EVERY RULE HERE IS SCOPED TO `body.home-quiet` OR TO A `home-`
 * CLASSNAME THAT ONLY THE HOME PANEL CARRIES. That is not tidiness — it is
 * what lets this land beside the record card, the television and the tape
 * deck without touching any of them. If a rule here ever needs to lose its
 * scope, it is no longer a home-page change and the suite will not catch it.
 *
 * ⚠️ 38.8 / 61.2, NOT 40/60. The seam sits off-centre on purpose so the
 * photograph dominates; a half-and-half split reads as a comparison rather
 * than as a poster with a caption.
 *
 * ⚠️ `.plate.full` IS FIXED TO THE WINDOW HERE. Everywhere else it is a
 * band between the header and the page edge. Fixed positioning is what lets
 * the photograph run under the index — and it is why `body.home-quiet` has
 * to be REMOVED on every other page rather than merely overridden.
 *
 * From Nick's design refresh, 26 Sep 2026 — "The image", his pick of four.
 */
/* Concept 4: the firm 39/61 split is the composition. The photograph is a
   single right-hand panel, with a hairline seam, not a page-wide texture. */
body.home-quiet, body.home-quiet .shell, body.home-quiet .body { background: var(--ground); }
body.home-quiet .shell { padding: 0; }
/* ⚠️ THE NAME MUST LAND IN THE SAME PLACE ON EVERY PAGE. Nick: *"Placement of
   Fergus Oswald is slightly different on home page compared to others so it
   jumps slightly."* It did, and by a lot more than it looked.

   Home zeroes `.shell`'s padding so the photograph can run to the edge, then
   put its own inset on `.top` — 30-76px against the 16-34px every other page
   inherits from the shell. Two numbers that were never compared.

   ⚠️ SO THE INSET IS NOW THE SHELL'S OWN, ADDED BACK BY HAND rather than
   guessed at: the same clamps the shell and the ordinary `.top` use, summed.
   Written as `calc()` of the originals so it cannot drift if either changes —
   a copied number here is the whole fault repeating. */
body.home-quiet .top {
  border: 0;
  padding: calc(clamp(14px, 2.2vh, 26px) + clamp(14px, 3.2vh, 40px))
           clamp(16px, 2.6vw, 34px) 0;
  align-items: baseline;
}
/* ⚠️ NOTHING LEFT TO SAY HERE. The wordmark is lowercase in the header face
   on every page from 26 Sep, so this rule was repeating the base one. It is
   removed rather than emptied — a rule that sets nothing is a rule somebody
   puts something back into. */
body.home-quiet .index { gap: clamp(20px, 4vw, 66px); padding: 0; }
/* ⚠️ ONLY THE COLOUR IS SET HERE NOW. The face, the case and the size moved
   into the base `.index a` rule on 26 Sep, because they are the whole site's
   now and not home's. What is left is the one thing that really is local: the
   header sits directly on the photograph here, so it takes the full paper
   colour rather than --dim. */
body.home-quiet .index a { color: var(--paper); padding: 0; }
body.home-quiet .index a:hover { color: var(--flame); }
body.home-quiet .index a::before, body.home-quiet .index a::after { content: none; }

/* ---- four corrections the refresh package did not carry -------------------
 *
 * Every one of these was found by RENDERING the page at 1440x900 and looking
 * at it. None of them fails a test, and three of them are wrong in the design
 * package's own preview file as well — so they are not transcription errors,
 * they are faults being inherited.
 */

/* ⚠️ 1. THE STRAPLINE IS OFF ON HOME. "Singer / Songwriter / Instrumentalist /
   Edinburgh, Leeds" is in the header on every page and belongs there — but on
   this page it lands in the top-right corner of the photograph, where the
   design has nothing at all. It is not deleted; it is the one page it is off. */
/* ⚠️ THE TAGLINE IS ON THE HOME PAGE TOO, from 26 Sep 2026 — Nick: *"It also
   needs to be on homepage."* It was hidden there because, as four middot-
   separated words in the top-right corner, it landed on the photograph where
   the design has nothing. As a two-line sentence under the name it is in the
   same place as on every other page, which is the point. */

/* ⚠️ 2. THE INDEX SITS BESIDE THE WORDMARK, not under it. Everywhere else the
   header is two stacked rows — name, then index below a rule. On home they are
   one row: name hard left, the five words hard right, which is the whole
   composition of the top edge. `.top-row` keeps `.burger` for the phone. */
body.home-quiet .top { display: flex; align-items: baseline; justify-content: space-between; gap: 24px; }
body.home-quiet .top-row { flex: 0 0 auto; }
body.home-quiet .index { flex: 0 1 auto; justify-content: flex-end; }

/* ⚠️ 3. "fergus oswald" NEEDS ITS SPACE BACK. The wordmark is `Fergus<br>Oswald`
   and this page hides the `<br>`, which closes the two words up into
   "fergusoswald" — visible in the first render and in the package's own
   shipped file. The break becomes a space rather than disappearing.
   `display: none` on a `<br>` removes it; `content: " "` puts a space there. */
body.home-quiet .wordmark br { display: none; }

/* ⚠️ 4. "join the list" WAS LANDING ON THE BROWN PANEL, not over the
   photograph — in this graft AND in the design package's own preview.
 
   THE CAUSE IS NOT THE POSITIONING. `.home-letter` is `position: fixed` and
   correct. `.panel` carries `animation: slidein ... both`, `slidein` animates
   `transform`, and a fill-mode of `both` leaves that transform applied for
   good — which makes the panel a containing block for any `fixed` descendant.
   So "fixed" was measuring from the 38.8% brown column instead of the window.
 
   ⚠️ SO THE FIX IS THE ANIMATION, NOT THE LINK. Home fades in rather than
   sliding up: no transform, no containing block, and the letter reaches the
   corner it was always asking for. Every other page still slides. The poster
   arriving without movement also suits it better than a slide did. */
body.home-quiet .panel { animation-name: fadein; }
body.home-quiet .body { padding: 0; }
body.home-quiet .pane { padding: 0; text-shadow: none; }
body.home-quiet .plate.full { position: fixed; inset: 0 0 0 auto; width: 61.2%; border-left: 1px solid rgba(220,100,57,.48); }
body.home-quiet .plate.full img, body.home-quiet .plate.full video {
  filter: saturate(1.12) brightness(1.28) contrast(1.04);
  object-position: center center !important;
}
body.home-quiet .plate.full::before { background: linear-gradient(180deg, rgba(0,0,0,.56) 0%, transparent 29%, transparent 80%, rgba(0,0,0,.22)); }
body.home-quiet .plate.full::after { background: none; }
.home-quiet-page { position: relative; width: 38.8%; height: 100%; min-height: 0; display: flex; flex-direction: column; justify-content: flex-end;
  padding: 0 24px clamp(80px, 9.5vh, 100px) clamp(32px, 5.3vw, 78px); }
.home-note { max-width: 390px; }


/* Two ways off the front page, side by side rather than stacked — stacked,
   they read as a list of everything rather than as two doors. */
.home-ways { display: flex; gap: clamp(16px, 2.6vw, 34px); flex-wrap: wrap; }
.home-note h1 { margin: 0 0 15px; font: italic normal clamp(30px, 2.55vw, 37px)/1.05 var(--mark); letter-spacing: -.035em; }
/* ⚠️ THE LINK INSIDE THIS LINE TAKES THE PAPER COLOUR, not the muted one the
   sentence around it uses — otherwise "Listen" reads as part of the caption
   rather than as the thing to press. */
.home-note p a { color: var(--paper); text-decoration: none; }
.home-note p a:hover { color: var(--flame); }
/* ⚠️ THE KIND IS A KICKER NOW, ABOVE THE TITLE — 28 Sep 2026. It carries no
   rule of its own: a line under it would separate the record's kind from the
   record's name, which are one thought. */
.home-note .home-kind { width: max-content; margin: 0 0 6px; padding: 0; border: 0;
  color: var(--dim); font: italic clamp(14px, 1.15vw, 17px)/1.2 var(--mark); }
/* ⚠️ AND LISTEN CLOSES THE BLOCK, so the rule that used to sit under the kind
   now sits under the whole record — kind, title and the way to hear it. */
.home-note .home-listen { width: max-content; min-width: 265px; margin: 0 0 22px; padding-bottom: 8px;
  border-bottom: 2px solid var(--flame); }
.home-note a { color: var(--paper); text-decoration: none; font: italic clamp(15px, 1.2vw, 18px)/1.2 var(--mark); }
.home-note a span, .home-letter span { color: inherit; }
.home-note a:hover, .home-letter:hover { color: var(--flame); }
.home-letter { position: fixed; right: clamp(45px, 8.5vw, 130px); bottom: clamp(66px, 9vh, 95px); z-index: 2;
  color: var(--paper); font: italic clamp(14px, 1.2vw, 18px)/1.2 var(--mark); text-decoration: none; }
@media (max-width: 760px) {
  body.home-quiet .shell { padding: 0; }
  body.home-quiet .top { padding: 25px 25px 0; }
  body.home-quiet .burger span { background: var(--paper); }
  body.home-quiet .index { background: var(--ground); padding: 12px 25px 20px; left: 0; right: 0; }
  body.home-quiet .index a { padding: 9px 0; font-size: 18px; }
  body.home-quiet .plate.full { width: 100%; border: 0; }
  body.home-quiet .plate.full img, body.home-quiet .plate.full video { object-position: center 45% !important; }
  body.home-quiet .plate.full::before { background: linear-gradient(180deg, rgba(42,30,20,.45), transparent 31%, rgba(42,30,20,.23) 51%, var(--ground) 82%); }
  .home-quiet-page { width: 100%; padding: 0 28px 90px; }
  .home-note h1 { font-size: clamp(34px, 8vw, 48px); }
  .home-note p { min-width: 0; width: 250px; }
  .home-letter { left: 28px; right: auto; bottom: 34px; font-size: 15px; }
}
