/* Generated by tools/build-css.mjs from assets/css/main.css — do not edit.
 *
 * Edit the part named in the marker above each block, then run: npm run build
 * An edit made here is lost at the next build, and a drift check that rebuilds this file and diffs
 * it fails in the meantime.
 */

/* assets/css/base/tokens.css */
/* ---------------------------------------------------------------
   neuro.SYS
   accent      = purple (#9a6bff) — interactive highlights
   accent-deep = purple (#6a00ff) — the same hue as a filled ground
   accent-2    = pink   (#ff2eab) — emphasis / the logo dot

   The accent was #6a00ff for everything, and as a *foreground* that measured 2.85:1 on --bg,
   2.63 on --surface and 2.44 on --surface-2 — under WCAG AA's 4.5:1 for text and under the 3:1
   a focus indicator needs. It carries every link, the focus ring, the home page's one call to
   action, the terminal cursor and the download arrow, so almost every place it appeared was the
   failing case. Lightened it reads 5.51 / 5.09 / 4.72.

   The deep purple was not wrong, it was wrong in front. Behind white text it measures 5.64:1, so
   it keeps the one job it was good at: the filled button in utilities.css. Same reasoning
   SoundCloudWidget.ACCENT already records for the player, where #6a00ff read as near-black
   against SoundCloud's own dark chrome.
   ---------------------------------------------------------------- */

:root {
  --bg:          #0b0c10;
  --surface:     #15161d;
  --surface-2:   #1c1d27;
  --border:      #262732;
  --fg:          #e8e8f0;
  --muted:       #84849c;
  --accent:      #9a6bff;
  --accent-deep: #6a00ff;
  --accent-2:    #ff2eab;

  --mono: "IBM Plex Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
  --sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* assets/css/base/elements.css */
/* The plain HTML elements, before anything names itself. Nothing here selects a class or a
   custom tag, which is what makes it base rather than a component. */

* { box-sizing: border-box; }

html { color-scheme: dark; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--sans);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

a {
  color: var(--accent);
  text-decoration: none;

  &:hover,
  &:focus-visible { text-decoration: underline; }
}

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

/* assets/css/layout/shell.css */
/* The site shell — Layout::header(), the #content main, Layout::footer(). Everything here is
   emitted by Layout.php and by nothing else, which is why it is one part. */

.site-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.5rem 1.5rem 0;
  max-width: 960px;
  margin: 0 auto;

  .logo {
    font-family: var(--mono);
    font-weight: 600;
    font-size: 1rem;
    color: var(--fg);
    letter-spacing: 0.02em;

    &:hover,
    &:focus-visible { text-decoration: none; }
  }

  .site-nav {
    display: flex;
    gap: 1.5rem;
    font-family: var(--mono);
    font-size: 0.8rem;
    text-transform: lowercase;
    letter-spacing: 0.06em;

    a {
      color: var(--muted);

      &:hover,
      &:focus-visible { color: var(--fg); text-decoration: none; }
    }
  }
}

/* Not nested: the logo dot is also the accent in HomeView's eyebrow. */
.logo-dot { color: var(--accent-2); }

#content {
  max-width: 960px;
  margin: 0 auto;
  padding: 1.5rem 1.5rem 4rem;

  /* Navigation focuses it after a swap, so the next Tab starts in the new page. It is no control,
     and a ring around the whole page would say it was one. */
  &:focus { outline: none; }
}

/* Locally vendored brand assets only. Hot-linking a platform's own CDN would
   transfer visitor data on page load, before any click (CJEU C-40/17). */
.site-footer {
  max-width: 960px;
  margin: 0 auto;
  padding: 0 1.5rem 2.5rem;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.8rem;

  .profile-links {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 1.1rem;
    margin-bottom: 1rem;
  }

  .profile-link {
    display: inline-flex;
    align-items: center;
    opacity: 0.75;
    transition: opacity 0.15s ease;

    &:hover,
    &:focus-visible { opacity: 1; }

    img {
      display: block;
      width: auto;
    }
  }
}

/* assets/css/layout/utilities.css */
/* The classes more than one view uses. A class that only ever appears on one page belongs in that
   view's part instead — this file is for the ones that would otherwise be written twice. */

.muted { color: var(--muted); }
.bang  { color: var(--accent-2); }

.page-section { margin: 1.5rem 0; }

.page-heading {
  font-family: var(--mono);
  font-size: 0.85rem;
  text-transform: lowercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin: 0 0 1rem;
}

.btn-primary {
  display: inline-block;
  font-family: var(--mono);
  font-size: 0.9rem;
  color: var(--accent);
  border: 1px solid var(--accent);
  padding: 0.55rem 1.1rem;
  border-radius: 6px;
  transition: background 0.15s ease, color 0.15s ease;

  /* The deep purple goes behind the text rather than in front of it — 5.64:1 against --fg, where
     the same colour as a foreground was 2.85:1. See base/tokens.css. */
  &:hover,
  &:focus-visible {
    background: var(--accent-deep);
    border-color: var(--accent-deep);
    color: var(--fg);
    text-decoration: none;
  }
}

/* assets/css/views/home.css */
/* HomeView. */

.home-hero {
  padding: 4rem 0 2rem;

  .home-eyebrow {
    font-family: var(--mono);
    font-size: 0.85rem;
    color: var(--muted);
    letter-spacing: 0.08em;
    margin: 0 0 0.5rem;
  }

  .home-title {
    font-family: var(--mono);
    font-size: clamp(2rem, 7vw, 3.5rem);
    font-weight: 600;
    letter-spacing: -0.02em;
    margin: 0 0 2rem;
    line-height: 1.1;
  }
}

/* assets/css/views/release.css */
/* ReleaseView — the classes the view writes itself. The boxes inside the hero grid are
   <cover-art> and <soundcloud-player>, and they style themselves; see elements/. */

.hero {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
  margin: 1.5rem 0 2.5rem;

  @media (min-width: 800px) {
    grid-template-columns: 1.1fr 1fr;
    align-items: stretch;
  }
}

.release-info {
  h1 {
    font-family: var(--mono);
    font-size: clamp(2rem, 6vw, 3.25rem);
    font-weight: 600;
    margin: 0;
    letter-spacing: -0.01em;
  }

  .tagline {
    margin: 0.25rem 0 1.75rem;
    color: var(--muted);
    font-family: var(--mono);
    font-size: 0.85rem;
    letter-spacing: 0.04em;
  }
}

.back-home { margin-top: 1.5rem; }

/* assets/css/views/demo.css */
/* DemoView — the gated page a demo is sent as. No cover, so no grid: one terminal, then the mixes.

   The <audio> here is the browser's own control and is deliberately left mostly alone. It is the
   one player on the site that is not a custom element (see DemoView), and the reason is that its
   built-in behaviour — seeking, keyboard, screen reader, working with JS off — is the feature.
   Restyling it means replacing it, which would spend all four. */

.demo-hero { margin: 1.5rem 0 2rem; }

.demo-info {
  h1 {
    font-family: var(--mono);
    font-size: clamp(2rem, 6vw, 3.25rem);
    font-weight: 600;
    margin: 0;
    letter-spacing: -0.01em;
  }

  .tagline {
    margin: 0.25rem 0 1.25rem;
    color: var(--muted);
    font-family: var(--mono);
    font-size: 0.85rem;
    letter-spacing: 0.04em;
  }
}

/* The one sentence on the page that explains why the page was hard to reach. Accent-2 rather than
   accent, because accent is what every link and control already uses — this is not one. */
.demo-notice {
  font-family: var(--mono);
  font-size: 0.8rem;
  line-height: 1.6;
  color: var(--accent-2);
  border-left: 2px solid var(--accent-2);
  padding: 0.1rem 0 0.1rem 0.75rem;
  margin: 0 0 1.75rem;
}

.demo-tracks {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* Label and duration on one row, the control across the full width under them. The duration is
   optional — DemoView omits the element entirely where nothing read one — so it is placed by
   `margin-left: auto` rather than by a grid column that would leave a gap when absent.

   The selector has to match two tags. Where a mix has a waveform DemoView emits <demo-waveform> and
   where it has none it emits a <div>, both carrying this class, so everything about the box is
   stated once here and only the canvas is in elements/waveform.css. A browser applies these rules
   to a custom element whether or not the script that upgrades it ever arrives, which is what keeps
   a JS-off demo card exactly the card it always was.

   The height is what the waveform needs and the row does not: min-height leaves the drawing
   somewhere to be, and a card with no waveform simply has more air in it. It is 11rem because of
   what is measurably in the way — `align-content: space-between` puts the label row at the top and
   the control at the bottom, and at 8.5rem the card's centre line, which is where the waveform is
   at its loudest and most colourful, falls exactly on the top edge of the
   control. */
.demo-track {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.75rem 0.9rem;
  min-height: 11rem;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  align-content: space-between;
  gap: 0.5rem;

  audio {
    flex-basis: 100%;
    width: 100%;
    margin-top: 0.6rem;
  }

  /* Above the canvas, which sits at z-index -1 inside the same box. Without a stacking position of
     their own these paint under it and the label reads through a drop.

     The halo is the other half of that. A column at full level reaches the top edge of the card, so
     a track that opens loud puts near-white waveform behind the label — #e8e8f0 on that measures
     about 2.6:1, under AA's 4.5. A plug of the card's own colour behind the glyphs costs nothing
     when there is no waveform under them and fixes the case where there is. The <audio> control
     needs none: the browser paints it opaque. */
  .demo-label,
  .demo-time {
    position: relative;
    text-shadow: 0 0 0.35rem var(--surface), 0 0 0.2rem var(--surface), 0 0 0.1rem var(--surface);
  }

  audio {
    position: relative;
  }
}

.demo-label {
  font-family: var(--mono);
  font-size: 0.9rem;
  color: var(--fg);
  letter-spacing: 0.04em;
}

.demo-time {
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--muted);
  margin-left: auto;
}

/* assets/css/views/stats.css */
/* StatsView. */

.stats-sub {
  font-family: var(--mono);
  font-size: 0.8rem;
  text-transform: lowercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  margin: 1.5rem 0 0.5rem;
}

.stats-table {
  border-collapse: collapse;
  font-family: var(--mono);
  font-size: 0.85rem;
  width: 100%;
  max-width: 480px;

  td {
    padding: 0.3rem 0.6rem 0.3rem 0;
    color: var(--fg);
    border-bottom: 1px solid var(--border);
  }
}

/* Deliberately not nested inside .stats-table: `.stats-table td` already outweighs
   this rule, and scoping it would flip that — a rendering change, not a cleanup.
   The !important below is the existing workaround for the same collision. */
.stats-count {
  text-align: right;
  color: var(--accent);
  padding-right: 0 !important;
}

/* assets/css/elements/card.css */
/* ---------------------------------------------------------------
   The card — the catalogue entry and the download entry share a look.

   The one part named for a concept rather than a component, because the look genuinely spans
   assets/ts/elements/release/ and .../download/. HtmlTest pins that list to this file alone, so a
   second concept-named part has to be argued for in a test named for the fact — the same idiom as
   the one call site allowed to parse markup this codebase did not write.

   Note what falls out of it: there is no elements/release.css, because every release-* tag is card
   and nothing else. <download-list> is the only download tag with a look of its own.
   ---------------------------------------------------------------- */

/* The wrappers name the fragment and nothing else; the anchor inside is still the
   card, so they are display: contents and add no box of their own. */
release-list,
release-card,
download-card {
  display: contents;

  a {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    padding: 0.9rem 1.1rem;
    margin-bottom: 0.6rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--fg);
    transition: border-color 0.15s ease, background 0.15s ease;

    &:hover,
    &:focus-visible {
      border-color: var(--accent);
      background: var(--surface-2);
      text-decoration: none;
    }
  }
}

release-title,
download-label {
  font-family: var(--mono);
  font-weight: 500;
  font-size: 0.95rem;
}

download-label::before { content: "↓ "; color: var(--accent); }

release-meta,
download-meta {
  font-size: 0.8rem;
  color: var(--muted);
}

/* assets/css/elements/terminal.css */
/* ---------------------------------------------------------------
   <terminal-window> and the five tags it builds — ReleaseView and NotFoundView.

   One part, because one component: a view writes <terminal-window> and the element builds the
   command, every row and the cursor. The row rule below spans three of those tags and needs no
   exception for it — they are all the same component. cf. assets/ts/elements/terminal/.
   ---------------------------------------------------------------- */

terminal-window {
  position: relative;
  /* Two columns the whole window shares: every row is a subgrid of them, so each value starts where
     the widest caption ends, in whichever language the captions came in. A fixed key width cannot
     say that — `gemacht mit` is eleven characters, and German is not the last language. */
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: 2ch;
  row-gap: 0.45rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  font-family: var(--mono);
  font-size: 0.875rem;
  /* top clears the title bar below (1.8rem tall + its 1px rule), then the body's own padding */
  padding: calc(1.8rem + 1px + 1rem) 1.1rem 1rem;

  &[narrow] { max-width: 480px; }

  /* The whole title bar is one pseudo-element: the label from the attribute, and the three
     traffic lights as gradients at fixed offsets. Decoration belongs in CSS, not in markup
     the server has to emit — <terminal-window> needs no children but its content. */
  &::before {
    content: attr(label);
    position: absolute;
    inset: 0 0 auto 0;
    box-sizing: content-box;
    height: 0.6rem;
    padding: 0.6rem 0.8rem;
    display: grid;
    place-items: center;
    color: var(--muted);
    font-size: 0.75rem;
    background-color: var(--surface-2);
    background-repeat: no-repeat;
    background-image:
      radial-gradient(circle at 1.1rem 0.9rem, #FF5F57 0.3rem, #0000 0),
      radial-gradient(circle at 2.1rem 0.9rem, #FEBC2E 0.3rem, #0000 0),
      radial-gradient(circle at 3.1rem 0.9rem, #28C840 0.3rem, #0000 0);
    border-bottom: 1px solid var(--border);
  }
}

/* The window builds all of these; a view only ever writes <terminal-window> itself. */
terminal-command,
terminal-field,
terminal-cursor {
  grid-column: 1 / -1;
  white-space: pre-wrap;
}

/* The $ sigil is decoration, so it is drawn rather than written out seven times. */
terminal-command::before,
terminal-cursor::before { content: "$ "; color: var(--accent-2); }

terminal-cursor::after {
  content: "_";
  display: inline-block;
  color: var(--accent);
  animation: blink 1s steps(1) infinite;

  @media (prefers-reduced-motion: reduce) { animation: none; }
}

terminal-field {
  /* The row takes the window's columns rather than sizing its own, which is what lines it up with
     the rows above and below; a long value wraps inside its column, not back under the key. */
  display: grid;
  grid-template-columns: subgrid;
  color: var(--muted);
  padding-left: 1.1rem;

  /* The tone lands on the row, and which half of it takes the accent is a styling decision rather
     than a markup one — ok reads as a value, error reads as a label. See TerminalTone. */
  &[tone="ok"] terminal-value { color: var(--accent); }
  &[tone="error"] terminal-key { color: var(--accent-2); }
}

terminal-key { color: var(--fg); }

/* Keyframes are global whatever file they sit in, so this one sits with its only user. */
@keyframes blink { 50% { opacity: 0; } }

/* assets/css/elements/CoverArt.css */
/* <cover-art> — a root-level element module, so the file is the component.
   cf. assets/ts/elements/CoverArt.ts, which builds the <img> this styles. */

cover-art {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 220px;

  img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}

/* assets/css/elements/embed.css */
/* Every consent-gated embed — add a provider's tag to this selector when you add its element.
   The element builds the gate itself and then replaces it with the embed in place, so there is no
   wrapper div and no server-rendered innards. With no JS it stays empty; the reserved height below
   still holds the space, so nothing reflows when the script lands.
   cf. assets/ts/elements/embed/.

   Two tags, one rule: <soundcloud-player> is one track and <soundcloud-profile> is the whole
   account's latest tracks, and the gate is identical either way — only the reserved height differs,
   and that arrives per element as an attribute rather than as a second block here. */

soundcloud-player,
soundcloud-profile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  margin-bottom: 2.5rem;
  /* Matches the iframe that replaces it, so the page doesn't jump on load.
     ConsentGatedEmbed sets --player-height from the element's own height attribute. */
  height: var(--player-height, 300px);
  border: 1px solid var(--border);
  border-radius: 8px;
  text-align: center;
  padding: 2rem;

  p     { color: var(--fg); margin: 0; }
  small { font-size: 0.8rem; color: var(--muted); }

  /* Set once the embed is in: the box stops being a gate and just holds the player. */
  &[loaded] {
    display: block;
    height: auto;
    padding: 0;
    border: 0;
  }

  iframe { border-radius: 8px; display: block; }
}

/* assets/css/elements/download.css */
/* The download component's own look. Its three other tags — <download-card>, <download-label>,
   <download-meta> — are the card, and live in card.css with the release side.
   cf. assets/ts/elements/download/. */

download-list {
  display: block;

  h2 {
    font-family: var(--mono);
    font-size: 0.85rem;
    text-transform: lowercase;
    letter-spacing: 0.08em;
    color: var(--muted);
    margin: 0 0 0.9rem;
  }
}

/* assets/css/elements/arrangement.css */
/* The arrangement: how a release is laid out in time, drawn from its own project's markers.
   Both tags — <release-arrangement> and <arrangement-section> — are guards that build nothing, so
   everything here is what the server already rendered. cf. assets/ts/elements/release/. */

release-arrangement {
  display: block;
  margin: 2rem 0 0;

  h2 {
    font-family: var(--mono);
    font-size: 0.85rem;
    text-transform: lowercase;
    letter-spacing: 0.08em;
    color: var(--muted);
    margin: 0 0 0.9rem;
  }
}

/* A row per section rather than a horizontal timeline, deliberately: the labels are free text a
   person typed, so `BUILDDOWN` and `SWITCH 1` have to be readable at any width, and a track's
   sections are not evenly spaced enough for a proportional bar to be worth the reflow. */
arrangement-section {
  display: grid;
  grid-template-columns: 3.5rem 1fr;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.4rem 0.7rem;
  border-left: 2px solid var(--border);
  background: linear-gradient(to right, var(--surface) 0%, transparent 60%);

  & + arrangement-section {
    margin-top: 2px;
  }

  .section-time {
    font-family: var(--mono);
    font-size: 0.8rem;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
  }

  .section-label {
    font-family: var(--mono);
    font-size: 0.85rem;
    letter-spacing: 0.06em;
    color: var(--fg);
  }
}

/* The accents. A section whose label matched no SectionKind carries no `kind` attribute at all, so
   none of these match and it draws in the plain style above — which is the point of letting
   SectionKind::classify() return null rather than inventing a case per spelling. */
arrangement-section[kind="intro"],
arrangement-section[kind="outro"] {
  border-left-color: var(--muted);
}

arrangement-section[kind="build"],
arrangement-section[kind="builddown"] {
  border-left-color: var(--accent-deep);
}

arrangement-section[kind="drop"] {
  border-left-color: var(--accent-2);

  .section-label {
    color: var(--accent-2);
  }
}

arrangement-section[kind="break"],
arrangement-section[kind="bridge"],
arrangement-section[kind="switch"] {
  border-left-color: var(--accent);
}

/* assets/css/elements/waveform.css */
/* <demo-waveform> — the canvas behind a demo's player, and the three colours it draws in.

   The element reads the properties below and nothing else about how it looks: which colour a band
   takes is a decision that belongs in a stylesheet, and CustomProperty.ts names them on the other
   side because no test can follow a custom property from where it is declared to where it is read.

   The card's own box — padding, border, the row of label and duration — is .demo-track in
   views/demo.css, which this element carries as a class. What is here is only what the waveform
   needs: a positioned box to be absolute inside, and a canvas that fills it. */

demo-waveform {
  position: relative;
  overflow: hidden;
  isolation: isolate;

  /* Purple for the sub, pink for the body, near-white for the air — the site's own accents rather
     than a CDJ's blue/orange/white, which would be the only three colours on the site that are not
     from tokens.css. */
  --wave-low: var(--accent);
  --wave-mid: var(--accent-2);
  --wave-high: var(--fg);

  canvas {
    position: absolute;
    inset: 0;
    z-index: -1;
    display: block;
    width: 100%;
    height: 100%;
    /* The waveform is behind text; at full strength the label is unreadable over a drop. */
    opacity: 0.5;
    /* A needle-drop target, so it says so — the native control below still does everything it did. */
    cursor: pointer;
  }
}
