/* =============================================================================
   song.com.im — one stylesheet, two editions
   -----------------------------------------------------------------------------
   The previous build was a dark neon console: Tailwind from a CDN, Font Awesome
   from a second CDN, forty accent colours, glass, glow, cards 530px tall. This
   is its opposite, and takes its register from labs.llc — paper, one ink, air,
   and type doing the work colour used to do.

   Both editions live in this one file. The tokens are declared twice: once on
   :root for paper, once under [data-theme="dark"]. Nothing below the token
   block names a colour directly, so the entire dark edition is those fourteen
   values and about thirty lines of correction where --ink is used as a FILL
   rather than as text — a dark button on a dark page has to invert, and no
   token can work that out for itself.

   Why one file and a runtime toggle, where labs.llc ships two documents: labs
   has two editions of one page, this has two editions of six. Twelve documents
   that must stay in lockstep is a maintenance trap, and the toggle costs one
   attribute on <html> written by four lines in the head before first paint.

   Contents
     1  webfont
     2  tokens — paper, then dark
     3  reset and type
     4  header, edition pill, era nav
     5  hero
     6  console — shuffle, search, chips
     7  spotlight
     8  track list
     9  footer, utility
   ========================================================================== */

/* ------------------------------------------------------------------ 1 font
   DM Sans, self-hosted, variable across weight 200-700 — the whole four-weight
   range in 94 KB of woff2 rather than twelve static faces. Split on the same
   unicode ranges Google splits on, so an English page never fetches the
   accented block. Both are preloaded from the head: a font discovered only
   when the parser reaches this rule is a font requested one round trip late. */
@font-face {
    font-family: 'DM Sans';
    font-style: normal;
    font-weight: 200 700;
    font-display: swap;
    src: url('../fonts/dm-sans-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                   U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                   U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'DM Sans';
    font-style: normal;
    font-weight: 200 700;
    font-display: swap;
    src: url('../fonts/dm-sans-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                   U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                   U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---------------------------------------------------------------- 2 tokens */
:root {
    --ink:        #1E262B;      /* near-black with the blue left in, never #000 */
    --ink-77:     rgba(30, 38, 43, .77);
    --ink-55:     rgba(30, 38, 43, .64);   /* 4.7:1 on paper — the body-text floor */
    --paper:      #FFFFFF;
    --paper-soft: #FAFAF8;      /* the ground a row sits on when it is not white */
    --deep:       #11161C;      /* the dark band inside the light edition */
    --line:       rgba(30, 38, 43, .12);
    --line-soft:  rgba(30, 38, 43, .07);
    --accent:     #C2410C;      /* hairlines and live marks only, never a fill */
    --accent-bg:  rgba(194, 65, 12, .09);

    --measure:    1240px;
    --gutter:     clamp(18px, 4.5vw, 48px);
    --header-h:   64px;
    --radius:     10px;
    --ease:       cubic-bezier(.22, .61, .36, 1);

    color-scheme: light;
}

/* The dark edition. Same names, different values — plus --paper and --deep
   swapping roles, because on a dark page the way to raise a surface is to
   lighten it, and --deep is the surface a card sits on. */
:root[data-theme="dark"] {
    --ink:        #FFFFFF;
    --ink-77:     rgba(255, 255, 255, .72);
    --ink-55:     rgba(255, 255, 255, .58);
    --paper:      #0C1116;
    --paper-soft: #10161C;
    --deep:       #1A222C;
    --line:       rgba(255, 255, 255, .16);
    --line-soft:  rgba(255, 255, 255, .09);
    --accent:     #F97316;      /* #C2410C is 2.4:1 on #0C1116 and unreadable */
    --accent-bg:  rgba(249, 115, 22, .13);

    color-scheme: dark;
}

/* ------------------------------------------------------------- 3 reset/type */
*, *::before, *::after { box-sizing: border-box; }

html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    scroll-padding-top: calc(var(--header-h) + 12px);
}
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

body {
    margin: 0;
    font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
    font-optical-sizing: auto;
    font-weight: 400;
    font-size: 16px;
    line-height: 1.55;
    color: var(--ink-77);
    background: var(--paper);
    /* The switch reads as a dimmer rather than a page swap. Every surface that
       resolves a token has to cross at this same .28s, or the page tears:
       <body> alone used to fade while the header, the console and the cards
       snapped, so for a quarter of a second the hero sat at the midpoint grey
       between two editions that had both already arrived. */
    transition: background-color .28s var(--ease), color .28s var(--ease);
}

/* The load-bearing rule of the whole sheet, lifted from labs: big text runs
   LIGHT and tight-tracked, small text runs HEAVY and positive-tracked. That
   inversion is what makes a large heading read as calm instead of loud. */
h1, h2, h3 {
    color: var(--ink);
    font-weight: 300;
    letter-spacing: -.03em;
    line-height: 1.06;
    margin: 0;
}
h1 { font-size: clamp(2.6rem, 7vw, 4.6rem); }
h2 { font-size: clamp(1.7rem, 3.4vw, 2.5rem); }
h3 { font-size: 1.06rem; font-weight: 600; letter-spacing: -.01em; }

p { margin: 0 0 1em; }
a { color: inherit; }

.eyebrow {
    font-size: .69rem;
    font-weight: 700;
    letter-spacing: .17em;
    text-transform: uppercase;
    color: var(--ink-55);
}

.wrap { max-width: var(--measure); margin-inline: auto; padding-inline: var(--gutter); }

/* Focus is drawn once, here, and never removed anywhere below. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

.vh {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.skip {
    position: absolute; left: 12px; top: -60px; z-index: 200;
    padding: .6rem 1rem; border-radius: var(--radius);
    background: var(--ink); color: var(--paper);
    font-size: .8rem; font-weight: 600; text-decoration: none;
    transition: top .2s var(--ease);
}
.skip:focus { top: 12px; }

/* ---------------------------------------------------------------- 4 header */
.hdr {
    position: sticky; top: 0; z-index: 100;
    height: var(--header-h);
    background: color-mix(in srgb, var(--paper) 88%, transparent);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border-bottom: 1px solid var(--line-soft);
    transition: background-color .28s var(--ease), border-color .28s var(--ease);
}
/* color-mix is recent enough that a fallback matters: without it the header is
   transparent and the track list scrolls through the wordmark. */
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
    .hdr { background: var(--paper); }
}
.hdr__in {
    max-width: var(--measure); margin-inline: auto; padding-inline: var(--gutter);
    height: 100%; display: flex; align-items: center; gap: 1.1rem;
}

.mark {
    display: inline-flex; align-items: baseline; gap: .1rem;
    font-size: 1.12rem; font-weight: 600; letter-spacing: -.045em;
    color: var(--ink); text-decoration: none; flex: 0 0 auto;
}
.mark b { font-weight: 600; }
.mark i { font-style: normal; color: var(--accent); }
.mark span { color: var(--ink-55); font-weight: 500; }

/* The era row. Scrolls sideways rather than wrapping, because a wrapped nav
   changes the header's height and the sticky offset stops being a constant. */
.eras {
    display: flex; align-items: center; gap: .1rem;
    margin-left: auto; min-width: 0;
    overflow-x: auto; overflow-y: hidden;
    scrollbar-width: none; -ms-overflow-style: none;
}
.eras::-webkit-scrollbar { display: none; }
.eras a {
    flex: 0 0 auto;
    padding: .42rem .62rem; border-radius: 7px;
    font-size: .78rem; font-weight: 600; letter-spacing: .01em;
    color: var(--ink-55); text-decoration: none;
    transition: color .2s var(--ease), background-color .2s var(--ease);
}
.eras a:hover { color: var(--ink); background: var(--line-soft); }
.eras a[aria-current="page"] { color: var(--ink); background: var(--line-soft); }

/* ------------------------------------------------- the edition switch
   A segmented control, not a checkbox and not another nav link: the two
   editions are alternatives to each other, and a segmented control is the one
   shape that says so without a word of explanation. Exactly one segment is
   lit; the thumb slides between them. A sun and a moon and nothing else —
   they need no legend, so the words live in aria-label and in .vh text. */
.pill {
    position: relative; flex: 0 0 auto;
    display: inline-flex; align-items: center;
    padding: 3px; border-radius: 999px;
    border: 1px solid var(--line);
    background: linear-gradient(180deg, var(--line-soft), transparent);
    box-shadow: inset 0 1px 2px var(--line-soft);
    transition: border-color .28s var(--ease);
}
.pill__thumb {
    position: absolute; z-index: 0;
    top: 3px; bottom: 3px; left: 3px; width: calc(50% - 3px);
    border-radius: 999px; background: var(--ink);
    box-shadow: 0 1px 2px rgba(0, 0, 0, .22), 0 6px 18px rgba(0, 0, 0, .12);
    transition: transform .42s var(--ease), background-color .28s var(--ease);
}
:root[data-theme="dark"] .pill__thumb { transform: translateX(100%); }

/* Hover previews the switch by sliding the thumb across early. :has is what
   lets the container answer for its children; where it is missing the pill
   simply does not preview, which costs nothing. */
.pill:has(.pill__seg[aria-pressed="false"]:hover) .pill__thumb { transform: translateX(100%); }
:root[data-theme="dark"] .pill:has(.pill__seg[aria-pressed="false"]:hover) .pill__thumb { transform: none; }

.pill__seg {
    position: relative; z-index: 1;
    display: inline-flex; align-items: center; justify-content: center;
    /* Both halves hold the same width: the thumb is half the track, and an
       uneven split would leave it short of one end. */
    min-width: 2.4rem; padding: .38rem .44rem;
    border: 0; border-radius: 999px; background: none;
    color: var(--ink-55); cursor: pointer;
    transition: color .32s var(--ease);
}
.pill__seg[aria-pressed="true"] { color: var(--paper); cursor: default; }
.pill__seg[aria-pressed="false"]:hover { color: var(--ink); }
/* While a segment is hovered the thumb sits under IT, so the lit colour has to
   follow the thumb rather than the pressed state. Equal specificity, later wins. */
.pill:has(.pill__seg:hover) .pill__seg[aria-pressed="true"] { color: var(--ink-55); }
.pill:has(.pill__seg:hover) .pill__seg:hover { color: var(--paper); }
/* 13px is written on the <svg> tag as width/height as well as here. An SVG with
   a viewBox and no dimensions falls back to 300x150 when no stylesheet reaches
   it — a sun the size of a fist in the middle of the header. */
.pill__i { width: 13px; height: 13px; display: block; flex: 0 0 auto; }

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

@media (max-width: 640px) {
    .mark span { display: none; }
    .pill__seg { min-width: 1.95rem; }
}

/* ------------------------------------------------------------------ 5 hero */
/* padding-block, not the padding shorthand: .hero is also .wrap, and the
   shorthand resets the inline padding .wrap set, which flattened the hero
   against the left edge while every other section stayed on the measure. */
.hero { padding-block: clamp(2.6rem, 7vw, 5rem) clamp(1.4rem, 3vw, 2.2rem); }
.hero__lede {
    max-width: 46ch; margin-top: 1.1rem;
    font-size: clamp(1rem, 1.6vw, 1.16rem); color: var(--ink-55);
}
.hero h1 em { font-style: normal; color: var(--accent); }

/* The counts under the title. A definition list, because that is what it is:
   four labelled numbers. */
.facts {
    display: flex; flex-wrap: wrap; gap: 0 clamp(1.5rem, 4vw, 3.2rem);
    margin: 2rem 0 0; padding-top: 1.5rem;
    border-top: 1px solid var(--line);
}
.facts div { min-width: 5.5rem; }
.facts dt { font-size: .66rem; font-weight: 700; letter-spacing: .16em;
            text-transform: uppercase; color: var(--ink-55); }
.facts dd { margin: .15rem 0 0; font-size: 1.6rem; font-weight: 300;
            letter-spacing: -.035em; color: var(--ink);
            font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------- 6 console
   Shuffle, search and the tag chips. Sticks under the header so the controls
   stay reachable 900 rows down the list. */
.console {
    position: sticky; top: var(--header-h); z-index: 90;
    padding: .85rem 0;
    background: color-mix(in srgb, var(--paper) 92%, transparent);
    backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
    border-block: 1px solid var(--line-soft);
    margin-bottom: 1.6rem;
    transition: background-color .28s var(--ease), border-color .28s var(--ease);
}
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
    .console { background: var(--paper); }
}
.console__row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }

.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
    padding: .62rem 1.1rem; border: 1px solid transparent; border-radius: var(--radius);
    background: var(--ink); color: var(--paper);
    font: inherit; font-size: .82rem; font-weight: 600; letter-spacing: .01em;
    text-decoration: none; cursor: pointer; white-space: nowrap;
    transition: opacity .2s var(--ease), transform .1s var(--ease),
                background-color .28s var(--ease), color .28s var(--ease);
}
.btn:hover { opacity: .84; }
.btn:active { transform: translateY(1px); }
.btn svg { width: 15px; height: 15px; flex: 0 0 auto; }

.btn--ghost { background: transparent; color: var(--ink); border-color: var(--line); }
.btn--ghost:hover { background: var(--line-soft); opacity: 1; }

/* The 5 / 10 / 25 group. A segmented control rather than three buttons: the
   three are the same action at three sizes, and sitting them in one track says
   so. It reads as an extension of Shuffle, which is what it is. */
.pickn {
    display: inline-flex; flex: 0 0 auto;
    border: 1px solid var(--line); border-radius: var(--radius);
    overflow: hidden; background: var(--paper-soft);
}
.pickn__b {
    font: inherit; font-size: .78rem; font-weight: 600;
    padding: .62rem .68rem; min-width: 2.5rem;
    border: 0; border-left: 1px solid var(--line);
    background: transparent; color: var(--ink-55); cursor: pointer;
    font-variant-numeric: tabular-nums;
    transition: background-color .18s var(--ease), color .18s var(--ease);
}
.pickn__b:first-child { border-left: 0; }
.pickn__b:hover { background: var(--ink); color: var(--paper); }
.pickn__b:active { opacity: .85; }

/* A family filter that is doing something is lit, the same way a pressed chip
   is — otherwise five dropdowns all look equally inert while one of them is
   the reason the list is 46 rows long. */
.sel--fam { font-size: .78rem; padding-block: .5rem; max-width: 15rem; }
.sel--fam.is-on {
    background: var(--ink); color: var(--paper); border-color: var(--ink);
}
/* The live filter said in words beside the control, because a <select> shows
   its value in the platform's own type and at the platform's own truncation —
   on a narrow screen "Drum & Bass · 17" becomes "Drum & B…" inside the box. */
.chips__now {
    font-size: .74rem; font-weight: 600; color: var(--ink-55);
    align-self: center; font-variant-numeric: tabular-nums;
}
.chips__now:empty { display: none; }

.search { position: relative; flex: 1 1 15rem; min-width: 0; }
.search svg {
    position: absolute; left: .72rem; top: 50%; transform: translateY(-50%);
    width: 15px; height: 15px; color: var(--ink-55); pointer-events: none;
}
.search input {
    width: 100%; padding: .62rem .72rem .62rem 2.2rem;
    border: 1px solid var(--line); border-radius: var(--radius);
    background: var(--paper-soft); color: var(--ink);
    font: inherit; font-size: .86rem;
    transition: background-color .28s var(--ease), border-color .28s var(--ease);
}
.search input::placeholder { color: var(--ink-55); }
.search input:focus { border-color: var(--ink-55); }
/* Safari draws its own round rect and a magnifier inside type=search. */
.search input::-webkit-search-decoration,
.search input::-webkit-search-cancel-button { -webkit-appearance: none; }

.sel {
    padding: .62rem 2rem .62rem .7rem;
    border: 1px solid var(--line); border-radius: var(--radius);
    background: var(--paper-soft); color: var(--ink);
    font: inherit; font-size: .82rem; font-weight: 500;
    cursor: pointer; appearance: none;
    /* The chevron is a background image so the control keeps one box. Its
       colour cannot be a var() inside a data URI, so it is drawn in currentColor
       via a mask instead — one grey that both editions can live with. */
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: calc(100% - 15px) center, calc(100% - 10px) center;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
}

/* The tag chips. A horizontal scroller on narrow screens rather than a wrap,
   so the console's height — which is a sticky offset — stays constant. */
/* Wraps now, rather than scrolling sideways. Six controls fit a line where
   twenty-nine chips never could, and a horizontal scroller on a desktop hides
   whatever is past the edge behind a gesture nobody makes — on the 90s page it
   cut off mid-word at "Drum & Bass". */
.chips { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem .5rem;
         margin-top: .6rem; }
/* The count sits at the far end of the filter row — beside the controls that
   change it, which is where it means something. */
.chips .count { margin-left: auto; }
.chip {
    flex: 0 0 auto;
    padding: .34rem .68rem; border: 1px solid var(--line); border-radius: 999px;
    background: transparent; color: var(--ink-55);
    font: inherit; font-size: .74rem; font-weight: 600; letter-spacing: .015em;
    cursor: pointer; white-space: nowrap;
    transition: color .18s var(--ease), background-color .18s var(--ease),
                border-color .18s var(--ease);
}
.chip:hover { color: var(--ink); border-color: var(--ink-55); }
.chip[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.chip small { opacity: .55; margin-left: .3rem; font-size: inherit; font-variant-numeric: tabular-nums; }

.count { font-size: .76rem; font-weight: 600; color: var(--ink-55);
         font-variant-numeric: tabular-nums; margin-left: auto; white-space: nowrap; }

/* ------------------------------------------------------------- 7 spotlight
   Where a shuffled pick lands. Hidden until there is one — an empty panel
   holding the page open is a promise the page has not kept yet. */
.spot {
    border: 1px solid var(--line); border-radius: 14px;
    background: var(--paper-soft);
    padding: clamp(1.1rem, 2.6vw, 1.7rem);
    margin-bottom: 1.6rem;
    transition: background-color .28s var(--ease), border-color .28s var(--ease);
}
.spot[hidden] { display: none; }
.spot__top { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.spot__t {
    font-size: clamp(1.5rem, 3.6vw, 2.3rem); font-weight: 300;
    letter-spacing: -.035em; line-height: 1.1; color: var(--ink); margin: .5rem 0 .2rem;
}
.spot__a { font-size: 1rem; font-weight: 500; color: var(--ink); }
.spot__m { font-size: .78rem; color: var(--ink-55); margin-top: .35rem; }
.spot__links { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: 1.1rem; }

/* The spotlight's multi-track list. Denser than the main list — it is a set
   you are deciding about, not a catalogue you are browsing — and it keeps the
   row shape so the two do not read as different kinds of thing. */
.picks { list-style: none; margin: 1rem 0 0; padding: 0; }
.pick {
    display: grid; grid-template-columns: 1.6rem minmax(0, 1fr) 3.2rem auto;
    align-items: center; gap: 0 .8rem;
    padding: .5rem 0; border-top: 1px solid var(--line-soft);
}
.pick__n { font-size: .72rem; color: var(--ink-55); font-variant-numeric: tabular-nums; }
.pick__m { min-width: 0; }
.pick__t, .pick__a { display: block; overflow: hidden; text-overflow: ellipsis;
                     white-space: nowrap; }
.pick__t { font-size: .9rem; font-weight: 500; color: var(--ink); }
.pick__a { font-size: .76rem; color: var(--ink-55); }
.pick__y { font-size: .76rem; color: var(--ink-55); text-align: right;
           font-variant-numeric: tabular-nums; }
.pick__l { display: flex; gap: .2rem; }

@media (max-width: 640px) {
    .pick { grid-template-columns: 1.4rem minmax(0, 1fr) auto; }
    .pick__y { display: none; }
    .pick__l { grid-column: 2 / -1; margin-top: .3rem; }
}

/* ------------------------------------------------------------ 8 track list
   A list, not a grid of cards. At a thousand rows a card wall is a scroll
   with no landmarks; a list gives every row the same shape, so the eye reads
   down one column instead of hunting across three. */
.list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line-soft); }

.trk {
    display: grid;
    grid-template-columns: 2.6rem minmax(0, 1fr) minmax(0, 13rem) 3.4rem auto;
    align-items: center; gap: 0 1rem;
    padding: .62rem .5rem;
    border-bottom: 1px solid var(--line-soft);
}
.trk:hover { background: var(--paper-soft); }

.trk__n { font-size: .72rem; color: var(--ink-55); font-variant-numeric: tabular-nums;
          text-align: right; }
.trk__main { min-width: 0; }
/* Both are spans inside one grid cell, so they need to be told to stack — as
   inline elements the title and the artist ran together on one line. */
.trk__t, .trk__a { display: block; }
.trk__t {
    font-size: .93rem; font-weight: 500; color: var(--ink);
    /* One line, clipped. A title that wraps changes the row height, and rows of
       two different heights are what make a long list feel unsteady. */
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.trk__a { font-size: .78rem; color: var(--ink-55);
          overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The album, where one is known. Quieter than the artist and set off by a
   middot rather than a label, because it is context rather than identity — and
   only about an eighth of the catalogue has one, so it must not leave a visible
   hole on the rows that do not. */
.trk__al { font-style: normal; }
.trk__al::before { content: ' \00b7 '; opacity: .55; }
.trk__g { font-size: .73rem; color: var(--ink-55);
          overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.trk__y { font-size: .78rem; color: var(--ink-55); font-variant-numeric: tabular-nums;
          text-align: right; }
.trk__links { display: flex; gap: .28rem; }

/* A track whose year is the artist's rather than the record's says so. */
.trk__y i, .spot__m i { font-style: normal; opacity: .6; }

.tag {
    display: inline-block; padding: .1rem .42rem; border-radius: 5px;
    background: var(--accent-bg); color: var(--accent);
    font-size: .66rem; font-weight: 700; letter-spacing: .04em;
    text-transform: uppercase; vertical-align: 1px;
}

/* The per-row link buttons. Icon-only at rest with the service in the tooltip
   and in .vh text — five word-labels per row across a thousand rows is noise,
   and the icons are the marks people already scan for. */
.lnk {
    display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; border-radius: 7px;
    border: 1px solid transparent; background: transparent;
    color: var(--ink-55); text-decoration: none;
    transition: color .16s var(--ease), background-color .16s var(--ease),
                border-color .16s var(--ease);
}
.lnk:hover { color: var(--ink); background: var(--line-soft); border-color: var(--line); }
.lnk svg { width: 15px; height: 15px; display: block; }
/* The one link that is a certainty rather than a search: a stored video id. */
.lnk--play { color: var(--accent); }

.spot__links .lnk {
    width: auto; height: auto; gap: .42rem;
    padding: .5rem .8rem; border-color: var(--line);
    font-size: .78rem; font-weight: 600; color: var(--ink);
}
.spot__links .lnk:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }

@media (max-width: 860px) {
    .trk { grid-template-columns: minmax(0, 1fr) auto; gap: 0 .7rem; padding: .7rem .35rem; }
    .trk__n, .trk__g { display: none; }
    /* Year rejoins the artist line rather than holding a column of its own. */
    .trk__y { grid-row: 1; grid-column: 2; text-align: right; align-self: center; }
    .trk__links { grid-column: 1 / -1; margin-top: .45rem; }
}

.sec       { padding-top: clamp(2.6rem, 6vw, 4.5rem); }
.sec--tight{ padding-top: 1rem; }
.sec h2    { margin-bottom: 1.4rem; }

code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: .88em; padding: .1em .3em; border-radius: 4px;
    background: var(--line-soft); color: var(--ink);
}
kbd {
    font-family: inherit; font-size: .82em; font-weight: 600;
    padding: .08em .38em; border: 1px solid var(--line); border-bottom-width: 2px;
    border-radius: 5px; color: var(--ink);
}

/* Narrow screens. The console is sticky, so its height is subtracted from
   every screenful of the list below it — at 375px it was wrapping into three
   rows and taking better than half the viewport. Search moves to a line of its
   own and the other three share the first, which is two rows instead of three
   and about 60px back. */
@media (max-width: 560px) {
    /* An explicit two-row grid rather than flex order and basis. Wrapping is
       decided from hypothetical sizes before any growing happens, so the four
       controls kept landing on three lines however the basis was tuned; naming
       the cells says what the layout is instead of arranging for it. */
    .console__row {
        display: grid;
        grid-template-columns: 1fr auto;
        grid-template-areas: "shuffle pickn"
                             "search  sort";
        gap: .5rem .6rem;
    }
    .console__row #shuffle { grid-area: shuffle; justify-self: start; }
    .console__row #pickn   { grid-area: pickn; justify-self: end; }
    .console__row .search  { grid-area: search; }
    .console__row .sel     { grid-area: sort; }
    /* Four facts wrap 3-then-1 in a flex row, which leaves a widow. */
    .facts { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.1rem 1rem; }
    .facts dd { font-size: 1.4rem; }
}

/* [hidden] is a UA rule at element specificity, so any class that sets display
   beats it. .spot carries its own guard for the same reason; without this one
   the Show-more button stayed on screen with nothing left to show. */
.more { display: flex; justify-content: center; padding: 1.8rem 0 0; }
.more[hidden], .empty[hidden] { display: none; }

.empty { padding: 3.5rem 1rem; text-align: center; color: var(--ink-55); }
.empty strong { display: block; color: var(--ink); font-size: 1.1rem; font-weight: 500;
                margin-bottom: .4rem; }
/* The load-failure diagnostic is a paragraph, not a line, and carries file
   paths that must not be allowed to run off a phone screen. */
.empty { max-width: 52ch; margin-inline: auto; }
.empty code { word-break: break-all; }
.btn:disabled { opacity: .4; cursor: not-allowed; }

/* --------------------------------------------------------- the era grid
   Used on the hub. Text-first: the era, its span, its count, its genres. */
/* The hairlines between cells are the cards' own outlines, not a coloured grid
   background showing through a 1px gap. The gap trick is shorter, but it paints
   every EMPTY cell too — five era cards in a two-column grid left a grey block
   next to techno.llc that read as a broken tile. An outline sits outside the
   card's box without affecting layout, two adjacent outlines land in the same
   1px gap and render as one line, and an empty cell stays paper. */
.grid {
    display: grid; gap: 1px; background: var(--paper);
    border: 1px solid var(--line); border-radius: 14px; overflow: hidden;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    transition: background-color .28s var(--ease), border-color .28s var(--ease);
}
.grid > * { outline: 1px solid var(--line); }
.card {
    display: flex; flex-direction: column; gap: .5rem;
    padding: clamp(1.2rem, 2.6vw, 1.8rem);
    background: var(--paper); color: inherit; text-decoration: none;
    transition: background-color .22s var(--ease);
}
.card:hover { background: var(--paper-soft); }

/* The era artwork, which the original site had and the first pass of this one
   dropped. It sits above the text, full-bleed to the card's padding, and it is
   decorative — the era name is right underneath it in text, so the alt is
   empty rather than a duplicate a screen reader would read twice.
   width/height are on the tag so the row reserves its space before the image
   arrives and the grid does not jump. */
.card__img {
    display: block; width: calc(100% + 2 * clamp(1.2rem, 2.6vw, 1.8rem));
    margin: calc(-1 * clamp(1.2rem, 2.6vw, 1.8rem));
    margin-bottom: .35rem;
    /* height:auto is required, not optional. The width/height attributes on the
       tag are presentational hints that set the CSS height to 483px, and
       aspect-ratio only governs when one axis is auto — without this the cards
       rendered the image at its full 483px regardless of how narrow they got. */
    height: auto; aspect-ratio: 3 / 2; object-fit: cover;
    transition: filter .35s var(--ease), transform .5s var(--ease);
}

/* ------------------------------------------------------- the paper print
   The artwork is neon on black: a lit thing, made for a dark ground. On the
   paper edition it sat in the page as five glowing rectangles with no
   relationship to anything around them.

   Inverting is what papers it. Greyscale turns the orange glow white; inverting
   then puts that glow on the page as INK and drops the black ground out to the
   paper — the picture arrives as something printed rather than something
   switched on. Order matters: greyscale must precede invert, or the invert
   produces the colour negative, cyan neon on white, and greyscaling that
   afterwards keeps the wrong luminances.

   The first version of this stopped there, at contrast 1.55, and was far too
   harsh — pure black ink slammed onto white, nothing like a print. What was
   missing is that a print is not neutral: paper is warm and ink is not pure
   black. So the contrast comes back down to 1, the brightness goes up to open
   the whole thing into the high key, and sepia + a small hue rotation carry it
   to copper. The result sits in the page instead of being stamped onto it.

   Dark keeps the artwork as shot. It was made for that ground.             */
:root:not([data-theme="dark"]) .card__img {
    filter: grayscale(1) invert(1) brightness(1.32) contrast(1)
            sepia(.45) saturate(1.5) hue-rotate(-12deg);
}

/* Hover splits the channels. #paper-ca — defined in the hub's markup — pulls
   R, G and B apart, offsets red and blue in opposite directions and screens
   them back, which is chromatic aberration done properly rather than a coloured
   overlay pretending to be it, plus a blurred copy screened over for bloom.

   It runs LAST in the chain, after the duotone. Run first, the greyscale that
   follows flattens every fringe it just made and all that survives is the blur.

   The scale is not decoration. At the extreme left edge only red and green are
   still present and at the right edge only green and blue, so the split leaves
   a warm bar down one side and a cyan bar down the other. The card clips, so
   growing the image fractionally puts both outside the visible box.          */
:root:not([data-theme="dark"]) .card:hover .card__img,
:root:not([data-theme="dark"]) .card:focus-visible .card__img {
    filter: grayscale(1) invert(1) brightness(1.32) contrast(1)
            sepia(.45) saturate(1.5) hue-rotate(-12deg) url(#paper-ca);
    transform: scale(1.035);
}
:root[data-theme="dark"] .card__img { filter: grayscale(.34) brightness(.82); }
:root[data-theme="dark"] .card:hover .card__img { filter: brightness(.95); }
/* The card clips the image's own corners at the grid's rounded outside edge. */
.grid > .card { overflow: hidden; }

@media (prefers-reduced-motion: reduce) { .card__img { transition: none; } }
.card__n { font-size: clamp(2rem, 4vw, 2.7rem); font-weight: 300;
           letter-spacing: -.05em; line-height: 1; color: var(--ink); }
.card__s { font-size: .72rem; font-weight: 600; letter-spacing: .1em;
           text-transform: uppercase; color: var(--ink-55); }
.card__d { font-size: .84rem; color: var(--ink-55); margin: 0; flex: 1; }
.card__f { font-size: .74rem; font-weight: 600; color: var(--ink);
           display: inline-flex; align-items: center; gap: .35rem; }
.card__f svg { width: 12px; height: 12px; transition: transform .22s var(--ease); }
.card:hover .card__f svg { transform: translateX(3px); }

/* ------------------------------------------------------------- 8b toasts
   The notification stack. Bottom-right on a desktop, where it is out of the
   reading column; full-width along the bottom on a phone, where there is no
   spare column and a floating card would cover the list.

   Kept to the same materials as everything else: paper, one ink, a hairline,
   one accent. The only thing here that is not elsewhere in the sheet is the
   shadow, and a floating layer is the one place a shadow earns its place —
   it is what says "above the page" rather than "part of it".
   ------------------------------------------------------------------------ */
.toasts {
    position: fixed; z-index: 300;
    right: var(--gutter); bottom: var(--gutter);
    display: flex; flex-direction: column-reverse; gap: .6rem;
    width: min(24rem, calc(100vw - 2 * var(--gutter)));
    /* The stack must not eat clicks in the gaps between toasts. */
    pointer-events: none;
}
.toast { pointer-events: auto; }

.toast {
    position: relative; overflow: hidden;
    display: grid; grid-template-columns: auto 1fr auto; gap: .1rem .7rem;
    padding: .85rem .9rem;
    border: 1px solid var(--line); border-radius: 12px;
    background: var(--paper);
    box-shadow: 0 1px 2px rgba(0, 0, 0, .06), 0 12px 32px -8px rgba(0, 0, 0, .18);
    /* Enters from below and slightly small; the transform is what makes it read
       as arriving rather than blinking on. */
    opacity: 0; transform: translateY(10px) scale(.985);
    transition: opacity .26s var(--ease), transform .26s var(--ease);
}
:root[data-theme="dark"] .toast {
    background: var(--deep);
    box-shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 12px 34px -8px rgba(0, 0, 0, .6);
}
.toast.is-in  { opacity: 1; transform: none; }
.toast.is-out { opacity: 0; transform: translateY(6px) scale(.99); }

.toast__i { width: 17px; height: 17px; margin-top: .12rem; flex: 0 0 auto; color: var(--ink-55); }
.toast--ok    .toast__i { color: #15803D; }
.toast--warn  .toast__i { color: var(--accent); }
.toast--error .toast__i { color: #DC2626; }
:root[data-theme="dark"] .toast--ok    .toast__i { color: #4ADE80; }
:root[data-theme="dark"] .toast--error .toast__i { color: #F87171; }

.toast__c { min-width: 0; }
.toast__t { margin: 0; font-size: .86rem; font-weight: 600; color: var(--ink);
            letter-spacing: -.01em; }
.toast__b { margin: .2rem 0 0; font-size: .8rem; color: var(--ink-55); line-height: 1.45; }

/* The detail table. Two columns on anything wide enough, stacked below that —
   a 14-row report needs to stay readable on a phone. */
.toast__d {
    display: grid; grid-template-columns: minmax(0, auto) minmax(0, 1fr);
    gap: .12rem .7rem;
    margin: .6rem 0 0; padding-top: .6rem;
    border-top: 1px solid var(--line-soft);
    font-size: .72rem;
}
.toast__d dt { color: var(--ink-55); white-space: nowrap; }
.toast__d dd {
    margin: 0; color: var(--ink); font-weight: 500;
    font-variant-numeric: tabular-nums;
    /* User agents and URLs are long and have no spaces in the useful places. */
    overflow-wrap: anywhere;
}

.toast__a { margin-top: .7rem; display: flex; gap: .4rem; flex-wrap: wrap; }
.toast__btn {
    font: inherit; font-size: .74rem; font-weight: 600;
    padding: .38rem .7rem; border-radius: 7px; cursor: pointer;
    border: 1px solid var(--line); background: transparent; color: var(--ink);
    transition: background-color .18s var(--ease), border-color .18s var(--ease),
                color .18s var(--ease);
}
.toast__btn:hover { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.toast__ta {
    width: 100%; font: inherit; font-size: .7rem; line-height: 1.4;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    padding: .5rem; border: 1px solid var(--line); border-radius: 7px;
    background: var(--paper-soft); color: var(--ink); resize: vertical;
}

.toast__x {
    align-self: start; flex: 0 0 auto;
    width: 24px; height: 24px; padding: 0; margin: -.1rem -.15rem 0 0;
    display: inline-flex; align-items: center; justify-content: center;
    border: 0; border-radius: 6px; background: transparent;
    color: var(--ink-55); cursor: pointer;
    transition: color .16s var(--ease), background-color .16s var(--ease);
}
.toast__x:hover { color: var(--ink); background: var(--line-soft); }
.toast__x svg { width: 13px; height: 13px; display: block; }

/* The remaining-time hairline. Purely informative — the toast can be dismissed
   at any point and hovering cancels the timer, which removes this with it. */
.toast.has-timer::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
    background: var(--ink-55); transform-origin: left;
    animation: toast-timer var(--toast-ms, 4200ms) linear forwards;
}
@keyframes toast-timer { from { transform: scaleX(1); } to { transform: scaleX(0); } }

@media (max-width: 560px) {
    .toasts {
        left: var(--gutter); right: var(--gutter);
        bottom: calc(var(--gutter) / 2);
        width: auto;
    }
}
@media (prefers-reduced-motion: reduce) {
    .toast { transition: opacity .01ms; transform: none; }
    .toast.is-in, .toast.is-out { transform: none; }
    .toast.has-timer::after { animation: none; }
}

/* The self-check link in the footer. A button, because it does something to
   this page rather than going anywhere. */
.ftr__check {
    font: inherit; font-size: inherit; padding: 0;
    border: 0; border-bottom: 1px solid var(--line); background: none;
    color: var(--ink-55); cursor: pointer;
}
.ftr__check:hover { color: var(--ink); border-bottom-color: var(--ink-55); }

/* --------------------------------------------------------------- 9 footer */
.ftr {
    margin-top: clamp(3.5rem, 8vw, 6rem);
    border-top: 1px solid var(--line);
    padding: 2.4rem 0 3rem;
    font-size: .8rem; color: var(--ink-55);
}
.ftr__row { display: flex; flex-wrap: wrap; gap: 1rem 2rem; align-items: baseline; }
.ftr a { color: var(--ink-55); text-decoration: none; border-bottom: 1px solid var(--line); }
.ftr a:hover { color: var(--ink); border-bottom-color: var(--ink-55); }
.ftr__note { margin-top: 1.4rem; max-width: 62ch; font-size: .76rem; }

/* Printing a thousand-row list is a legitimate thing to want; the sticky
   chrome and the link buttons are not part of it. */
@media print {
    .hdr, .console, .spot__links, .trk__links, .more, .skip { display: none !important; }
    .trk { break-inside: avoid; }
    body { background: #fff; color: #000; }
}
