/*
  Bravith — main.css
  Design tokens, resets, typography, layout primitives.
*/

/* ---------- Design tokens ----------
   Bravith has ONE appearance. Every token is defined once, here on
   bare :root, and nothing overrides it — there is no second palette,
   no `prefers-color-scheme` branch and no `data-theme` attribute.
   The site looks the same to every visitor on every device.

   `color-scheme: light` is not a theme switch: it tells the browser
   to draw its own furniture (scrollbars, form controls, the canvas
   behind the page) to match the palette below, so a visitor whose OS
   is set to dark doesn't get dark scrollbars against a white page.
------------------------------------------------------------ */
:root {
  color-scheme: light;

  /* Color: surfaces */
  --color-bg: #ffffff;
  --color-bg-elevated: #ffffff;
  --color-surface: #ffffff;
  --color-surface-hover: #f2efea;
  --color-surface-active: #eae5de;
  --color-border: #e6e1d9;
  --color-border-strong: #d6cfc3;
  --color-code-bg: #f1ede6;
  --color-input-bg: #ffffff;
  --color-header-bg: rgba(255, 255, 255, 0.85);
  --color-scrim: rgba(32, 28, 24, 0.5);

  /* Color: text */
  --color-text: #201c18;
  --color-text-muted: #5a5450;
  --color-text-faint: #8c857d;
  --color-on-accent: #ffffff;

  /* Color: brand accent (coral) */
  --color-accent: #e2483f;
  --color-accent-hover: #c73c34;
  --color-accent-soft: rgba(226, 72, 63, 0.09);
  --color-accent-border: rgba(226, 72, 63, 0.32);

  /* Color: semantic */
  --color-secondary: #b5720a;
  --color-secondary-soft: rgba(181, 114, 10, 0.1);
  --color-warning: var(--color-secondary);
  --color-warning-soft: var(--color-secondary-soft);
  --color-info: #2467b3;
  --color-info-soft: rgba(36, 103, 179, 0.1);
  --color-success: #1c8a4f;
  --color-success-soft: rgba(28, 138, 79, 0.1);
  --color-danger: #c9372c;
  --color-danger-soft: rgba(201, 55, 44, 0.1);

  /* Color: category tints. PART OF THE GLOBAL DESIGN SYSTEM, not a homepage
     detail — the category pages, the archives and the homepage all read these
     same eight tokens.

     One identity per category, assigned in bravith_category_tint() rather
     than cycled, so a category is the same colour everywhere it appears — its
     hero panel, its filter chips, its badges, its tile in a list.

     Tints 1-6 each belong to one of the six categories, and each matches that
     category's artwork. 7 and 8 are unclaimed, held for a type added later.

     These carry small bold label text, so the light values are picked for
     >= 4.5:1 on the page background rather than for maximum saturation. */
  --home-tint-1: #1a7d49;  --home-tint-1-soft: rgba(26, 125, 73, 0.1);   /* green  — Mods */
  --home-tint-2: #6035b8;  --home-tint-2-soft: rgba(96, 53, 184, 0.1);   /* purple — Shaders */
  --home-tint-3: #c2372f;  --home-tint-3-soft: rgba(194, 55, 47, 0.09);  /* red    — Modpacks */
  --home-tint-4: #1f5fa8;  --home-tint-4-soft: rgba(31, 95, 168, 0.1);   /* blue   — Maps */
  --home-tint-5: #97600a;  --home-tint-5-soft: rgba(151, 96, 10, 0.1);   /* yellow — Texture Packs */
  --home-tint-6: #4d5766;  --home-tint-6-soft: rgba(77, 87, 102, 0.1);   /* slate  — Guides */
  --home-tint-7: #0d6f7c;  --home-tint-7-soft: rgba(13, 111, 124, 0.1);  /* teal   — unclaimed */
  --home-tint-8: #9c2f73;  --home-tint-8-soft: rgba(156, 47, 115, 0.1);  /* plum   — unclaimed */

  /* Typography */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-md: 1.125rem;
  --text-lg: 1.25rem;
  --text-xl: 1.5rem;
  --text-2xl: 1.875rem;
  --text-3xl: 2.25rem;
  --text-4xl: 2.75rem;

  --leading-tight: 1.2;
  --leading-normal: 1.55;

  /* Spacing */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-20: 5rem;
  --space-24: 6rem;

  /* Shape */
  --radius-xs: 9px;
  --radius-sm: 11px;
  --radius-md: 14px;
  --radius-lg: 16px;
  --radius-pill: 999px;

  /* Elevation */
  --shadow-sm: 0 1px 2px rgba(28, 22, 18, 0.08);
  --shadow-md: 0 8px 24px rgba(28, 22, 18, 0.1);
  --shadow-card: 0 1px 2px rgba(28, 22, 18, 0.04), 0 6px 16px rgba(28, 22, 18, 0.06);

  /* Layout */
  --container-width: 1240px;
  --container-width-wide: 1460px;
  --container-padding: var(--space-5);

  /* Motion */
  --transition-fast: 120ms ease;
  --transition-base: 200ms ease;
}

/* ---------- Reset ---------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  color: var(--color-text);
  background-color: var(--color-bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ---------- Page shell ----------
   THE FOOTER SITS AT THE BOTTOM OF A SHORT PAGE. The error pages are the only
   documents on the site shorter than a viewport — 404 and 403 render about
   1020px of page into a 1300px window — and the footer simply ended where the
   content did, leaving a band of bare background under it that read as a
   rendering fault rather than as the end of the page.

   A flex column with a viewport minimum, and the article column as the only
   growing item: on a short page the free space is handed to <main>, which
   pushes the footer to the bottom edge; on every normal page the container's
   height is its content and nothing grows, so a long document lays out exactly
   as before. Deliberately NOT position: fixed or absolute — the footer stays
   in flow and keeps scrolling with the page.

   svh, not vh: on a phone `100vh` is the viewport with the URL bar HIDDEN, so
   a page that exactly fits gains a scrollbar it does not need. The vh line is
   the fallback for anything that has not implemented the small-viewport unit.

   Scoped away from the admin, which loads this stylesheet too and lays its
   own shell out with #admin-main. The public pages are the ones with a bare
   <body> and a <main id="main">; the admin carries body.admin. The skip link
   is absolute and the search overlay is fixed, so neither becomes a flex item
   with a height of its own. */
body:not(.admin) {
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

body:not(.admin) > #main {
  flex: 1 0 auto;
}

img,
picture,
svg {
  max-width: 100%;
  display: block;
}

img {
  height: auto;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}

button {
  cursor: pointer;
  background: none;
  border: none;
}

a {
  color: inherit;
  text-decoration: none;
}

ul,
ol {
  list-style: none;
  padding: 0;
}

table {
  border-collapse: collapse;
}

h1, h2, h3, h4, h5, h6 {
  line-height: var(--leading-tight);
  font-weight: 700;
  text-wrap: balance;
}

@media (prefers-reduced-motion: reduce) {
  html:focus-within {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- Focus states ---------- */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

/* ---------- Typography helpers ---------- */
h1 { font-size: var(--text-3xl); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl); }
h4 { font-size: var(--text-md); }

p {
  color: var(--color-text-muted);
}

.lede {
  font-size: var(--text-md);
  color: var(--color-text-muted);
  max-width: 60ch;
}

.text-muted {
  color: var(--color-text-muted);
}

.text-faint {
  color: var(--color-text-faint);
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-accent);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: -48px;
  left: var(--space-4);
  z-index: 200;
  background: var(--color-accent);
  color: var(--color-on-accent);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  font-weight: 700;
  transition: top var(--transition-fast);
}

.skip-link:focus {
  top: var(--space-4);
}

/* ---------- Layout primitives ---------- */
.container {
  width: 100%;
  max-width: var(--container-width);
  margin-inline: auto;
  padding-inline: var(--container-padding);
}

.container-wide {
  width: 100%;
  max-width: var(--container-width-wide);
  margin-inline: auto;
  padding-inline: var(--container-padding);
}

.section {
  padding-block: var(--space-16);
}

.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-8);
}

.section-head h2 {
  margin-top: var(--space-2);
}

.section-intro {
  max-width: 62ch;
}

.grid {
  display: grid;
  gap: var(--space-6);
}

/* Single column at the base width; responsive.css steps these up to 2 at
   >=768px and 3 at >=1024px. The base has to be 1fr — a phone rendering three
   cards side by side leaves each one about 100px wide. */
.grid-3 {
  grid-template-columns: 1fr;
}

.grid-4 {
  grid-template-columns: repeat(4, 1fr);
}

.grid-2 {
  grid-template-columns: repeat(2, 1fr);
}

/* ---------- Category pages ----------
   A category page carries two kinds of content inside one container: editorial
   prose, which needs a capped measure to stay readable, and the resource
   listing, which uses the whole width. The base .prose cap is tuned for the
   guides and resource pages; here the prose runs full-width below the listing,
   so it is widened — scoped to .category-page so those other layouts are
   untouched.

   The hero lede is capped tighter still, because it sits in a column beside
   the artwork plate rather than across the page. */
.category-page {
  --category-reading: 850px;
}

.category-page .prose {
  max-width: var(--category-reading);
}

.category-page .category-hero-copy .lede {
  max-width: 56ch;
}

.divider {
  border: none;
  border-top: 1px solid var(--color-border);
  margin-block: var(--space-12);
}

.surface {
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

/* ---------- Header ---------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background-color: var(--color-header-bg);
  backdrop-filter: saturate(140%) blur(8px);
  border-bottom: 1px solid var(--color-border);
}

.site-header .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  height: 72px;
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 800;
  font-size: var(--text-lg);
  letter-spacing: 0.01em;
  color: var(--color-text);
  flex-shrink: 0;
}

.brand-mark {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
}

.brand-word {
  white-space: nowrap;
}

.brand-word .accent-dot {
  color: var(--color-accent);
}

.primary-nav {
  display: none;
}

.primary-nav ul {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.primary-nav a {
  display: inline-flex;
  align-items: center;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-xs);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  transition: color var(--transition-fast), background-color var(--transition-fast);
}

.primary-nav a:hover {
  color: var(--color-text);
  background-color: var(--color-surface-hover);
}

.primary-nav a[aria-current="page"] {
  color: var(--color-accent);
  background-color: var(--color-accent-soft);
}

.header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  transition: color var(--transition-fast), background-color var(--transition-fast);
}

.icon-btn:hover {
  color: var(--color-text);
  background-color: var(--color-surface-hover);
}

.icon-btn svg {
  width: 20px;
  height: 20px;
}

.nav-toggle {
  display: inline-flex;
}

/* ---------- Mobile nav ---------- */
.mobile-nav {
  position: fixed;
  inset: 0;
  top: 72px;
  background-color: var(--color-bg);
  border-top: 1px solid var(--color-border);
  padding: var(--space-6) var(--container-padding);
  overflow-y: auto;
  z-index: 99;
}

.mobile-nav[hidden] {
  display: none;
}

.mobile-nav ul {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.mobile-nav a {
  display: block;
  padding: var(--space-4) var(--space-2);
  font-size: var(--text-lg);
  font-weight: 700;
  border-bottom: 1px solid var(--color-border);
}

.mobile-nav a[aria-current="page"] {
  color: var(--color-accent);
}

/* ---------- Search overlay ----------
   The panel's heading, chip row and footer are built by initSearch() in
   js/main.js rather than shipped in the markup — see the comment there. The
   form, input, close button and results container come from the HTML. */
.search-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background-color: var(--color-scrim);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: min(12vh, var(--space-20)) var(--container-padding) var(--space-6);
}

.search-overlay[hidden] {
  display: none;
}

/* A column so the footer can sit against the bottom edge while the middle
   scrolls, and overflow:hidden so the footer's tinted band is cut by the
   card's own radius instead of squaring off its corners. */
.search-panel {
  width: 100%;
  max-width: 620px;
  max-height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background-color: var(--color-bg-elevated);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}

/* min-height: 0 IS WHAT KEEPS THE FOOTER ON SCREEN.
   A flex item defaults to min-height: auto, so this block refused to shrink
   below its content, grew past the panel's max-height and pushed the footer
   out — where the panel's overflow:hidden then clipped it away entirely. With
   a zero minimum it can shrink and scroll instead. In practice the results
   list below hits its own cap first; this only takes over on a short viewport. */
.search-panel-main {
  min-height: 0;
  padding: var(--space-8) var(--space-8) var(--space-6);
  overflow-y: auto;
}

.search-panel-head {
  position: relative;
  margin-bottom: var(--space-6);
  text-align: center;
}

.search-panel-title {
  font-size: var(--text-xl);
}

.search-panel-sub {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* Pulled out to the padding's edge so it sits in the card's corner rather than
   in the heading's line, which is where a reader looks for it. */
.search-panel-close {
  position: absolute;
  /* Negative by the panel's own padding, so the button lands in the card's
     corner rather than on the heading's line. */
  top: calc(var(--space-5) * -1);
  right: calc(var(--space-5) * -1);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-pill);
  color: var(--color-text-faint);
  transition: color var(--transition-fast), background-color var(--transition-fast);
}

.search-panel-close:hover {
  color: var(--color-text);
  background-color: var(--color-surface-hover);
}

.search-panel-close svg {
  width: 18px;
  height: 18px;
}

.search-panel form {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 56px;
  padding: var(--space-3) var(--space-5);
  background-color: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

/* The ring belongs to the field as a whole: the input inside it has no box of
   its own, so focusing it has to light up this border instead. */
.search-panel form:focus-within {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

.search-panel-icon {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  color: var(--color-text-faint);
}

.search-panel input[type="search"] {
  flex: 1;
  min-width: 0;
  background: none;
  border: none;
  color: var(--color-text);
  font-size: var(--text-md);
}

.search-panel input[type="search"]::placeholder {
  color: var(--color-text-faint);
}

.search-panel input[type="search"]:focus {
  outline: none;
}

/* Chrome draws its own clear button inside a search field once it has a value
   and focus — and the overlay focuses the field the moment it opens, so it
   would be on screen for the whole of every search, in the browser's blue,
   beside a red focus ring. Escape and the close button already empty the
   field; this is the one control the design cannot absorb. */
.search-panel input[type="search"]::-webkit-search-cancel-button {
  display: none;
}

/* Real category links, not a popularity ranking — see buildSuggestions() in
   js/main.js. Hidden the moment a query is typed, so the results have the
   panel to themselves. */
.search-panel-suggest {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-5);
}

.search-panel-suggest[hidden] {
  display: none;
}

.search-panel-suggest-label {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.search-chip {
  display: inline-flex;
  align-items: center;
  padding: 0.35rem 0.8rem;
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--color-text-muted);
  transition: color var(--transition-fast), border-color var(--transition-fast),
    background-color var(--transition-fast);
}

.search-chip:hover,
.search-chip:focus-visible {
  color: var(--color-accent);
  border-color: var(--color-accent-border);
  background-color: var(--color-accent-soft);
}

/* Capped so a broad query ("shader") cannot run the panel off the bottom of
   the screen. The list scrolls; the field and the footer stay put. */
.search-results {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-height: 340px;
  overflow-y: auto;
}

.search-results:not(:empty) {
  margin-top: var(--space-5);
}

.search-results a {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-3);
  border-radius: var(--radius-sm);
}

.search-results a:hover,
.search-results a:focus-visible {
  background-color: var(--color-surface-hover);
}

/* Scoped to the overlay's own list. The results PAGE has its own
   .search-result-title / .search-result-meta in components.css, which loads
   after this file and would otherwise restyle these rows from a mile away. */
.search-results .search-result-title {
  font-weight: 700;
  font-size: var(--text-sm);
}

.search-results .search-result-meta {
  display: block;
  margin: 0;
  font-size: var(--text-xs);
  font-weight: 400;
  color: var(--color-text-faint);
}

.search-empty {
  margin-top: var(--space-4);
  font-size: var(--text-sm);
  color: var(--color-text-faint);
}

.search-panel-foot {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--color-border);
  background-color: var(--color-surface-hover);
  font-size: var(--text-xs);
  color: var(--color-text-faint);
  text-align: center;
}

.search-panel-kbd {
  padding: 0.1rem 0.45rem;
  background-color: var(--color-bg-elevated);
  border: 1px solid var(--color-border-strong);
  border-bottom-width: 2px;
  border-radius: var(--radius-xs);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--color-text-muted);
}

@media (max-width: 560px) {
  .search-overlay {
    padding-top: var(--space-8);
  }

  .search-panel-main {
    padding: var(--space-6) var(--space-5) var(--space-5);
  }

  .search-panel-close {
    top: calc(var(--space-1) * -1);
    right: calc(var(--space-1) * -1);
  }
}

/* ---------- Hero ---------- */
.hero {
  padding-block: var(--space-16) var(--space-12);
  border-bottom: 1px solid var(--color-border);
  overflow: hidden;
}

.hero-layout {
  display: grid;
  gap: var(--space-8);
  align-items: center;
}

.hero-content {
  display: grid;
  gap: var(--space-6);
  max-width: 620px;
}

.hero h1 {
  font-size: var(--text-4xl);
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-2);
}

.hero-visual {
  display: none;
}

.hero-visual svg {
  width: 100%;
  height: auto;
  max-width: 420px;
  margin-inline: auto;
}

/* ---------- Page header (inner pages) ---------- */
.page-header {
  padding-block: var(--space-10) var(--space-8);
  border-bottom: 1px solid var(--color-border);
}

.page-header h1 {
  margin-top: var(--space-3);
  font-size: var(--text-3xl);
}

.page-header .lede {
  margin-top: var(--space-4);
}

.page-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  margin-top: var(--space-5);
  font-size: var(--text-sm);
  color: var(--color-text-faint);
}

.page-meta strong {
  color: var(--color-text-muted);
  font-weight: 600;
}

/* ---------- Footer ---------- */
.site-footer {
  border-top: 1px solid var(--color-border);
  background-color: var(--color-bg-elevated);
  padding-block: var(--space-16) var(--space-8);
}

/* One column at the base width; responsive.css steps this up to 2 at >=768px
   and to the full 1.4fr + 4 layout at >=1024px. Five columns plus four large
   gaps cannot fit a phone: the tracks stop shrinking at their content width
   and push the whole document sideways, which made every page on the site
   scroll horizontally on mobile. */
.footer-grid {
  display: grid;
  gap: var(--space-10);
  grid-template-columns: 1fr;
}

.footer-brand p {
  margin-top: var(--space-4);
  max-width: 34ch;
  font-size: var(--text-sm);
}

/* Column headings in the text colour rather than the faint one. They are the
   only structure a five-column footer has: at --color-text-faint they were
   the same weight on the page as the links beneath them, and the whole block
   read as one undifferentiated field of small grey type. */
.footer-col h3 {
  font-size: var(--text-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text);
  margin-bottom: var(--space-5);
}

.footer-col ul {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.footer-col a {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  transition: color var(--transition-fast);
}

.footer-col a:hover {
  color: var(--color-accent);
}

.footer-bottom {
  margin-top: var(--space-12);
  padding-top: var(--space-6);
  border-top: 1px solid var(--color-border);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: space-between;
  font-size: var(--text-xs);
  color: var(--color-text-faint);
}

.footer-bottom nav ul {
  display: flex;
  gap: var(--space-4);
}

.footer-bottom a:hover {
  color: var(--color-accent);
}

/* "Cookie settings" beside Privacy and Terms. A button, because it opens the
   consent dialog on this page rather than going anywhere, dressed as the
   links it sits between. The publisher only emits it when a consent-gated
   service is configured, so no page shows a control that does nothing. */
.footer-link-btn {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: color var(--transition-fast);
}

.footer-link-btn:hover {
  color: var(--color-accent);
}

/* ---------- Article prose ---------- */
.prose {
  max-width: 72ch;
}

.prose h2 {
  margin-top: var(--space-10);
  margin-bottom: var(--space-4);
}

.prose h3 {
  margin-top: var(--space-8);
  margin-bottom: var(--space-3);
}

.prose p {
  margin-bottom: var(--space-4);
}

.prose ul,
.prose ol {
  margin-bottom: var(--space-4);
  padding-left: var(--space-5);
}

.prose ul {
  list-style: disc;
}

.prose ol {
  list-style: decimal;
}

.prose li {
  color: var(--color-text-muted);
  margin-bottom: var(--space-2);
}

.prose li::marker {
  color: var(--color-accent);
}

.prose a {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--color-accent-border);
}

.prose a:hover {
  text-decoration-color: var(--color-accent);
}

.prose strong {
  color: var(--color-text);
}

.prose blockquote {
  border-left: 3px solid var(--color-accent);
  padding-left: var(--space-4);
  margin-block: var(--space-6);
  color: var(--color-text-muted);
  font-style: italic;
}

.prose code {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background-color: var(--color-code-bg);
  border: 1px solid var(--color-border);
  padding: 0.1em 0.4em;
  border-radius: var(--radius-xs);
}

/* A multi-line block, for commands a reader is meant to copy. A command line
   cannot wrap without changing what it says, so the block scrolls sideways on
   its own rather than pushing the page wider — at 320px an untreated <pre>
   dragged the whole document 131px past the viewport. */
.prose pre {
  margin-block: var(--space-6);
  padding: var(--space-4);
  background-color: var(--color-code-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  overflow-x: auto;
  max-width: 100%;
}

/* The inline treatment above would double the border and background inside a
   <pre>, so a nested <code> keeps only the typeface. */
.prose pre code {
  background: none;
  border: 0;
  padding: 0;
  border-radius: 0;
  font-size: 0.85em;
  line-height: 1.7;
  white-space: pre;
}


/* ==========================================================================
   THE INK BAND — dark top, curved transition, light content, dark footer
   --------------------------------------------------------------------------
   The visual identity is a single dark band across the top of every page
   (navigation, breadcrumbs and the page's hero), a curved transition into the
   light content area, and a dark footer closing the page.

   HOW THIS IS DONE, AND WHY THERE IS NO MARKUP CHANGE.
   Every component on the site already draws itself from the colour tokens.
   So the band is not a set of per-component overrides — it REDECLARES THE
   TOKENS on the handful of elements that form it. A badge, a button, a
   breadcrumb or a statistic placed inside the band re-themes itself, because
   it is reading --color-text and --color-border exactly as it always did.
   Nothing inside the band had to be given a rule of its own, and no published
   page had to be rewritten to gain the treatment.

   That is also what keeps it safe: a component added tomorrow inherits the
   band correctly with no extra work, and removing this whole section restores
   the previous appearance exactly.

   The band is near-black against the white page, and the structure — dark,
   curve, content, dark — is what gives every page type the same opening.
   ========================================================================== */

:root {
  /* The band's own palette. Deliberately not the page greys: the band is a
     fixed near-black, so the brand reads identically wherever it appears. */
  --ink-bg:           #0e0c0b;
  --ink-bg-elevated:  #17130f;
  --ink-surface:      rgba(255, 255, 255, 0.055);
  --ink-surface-hover:rgba(255, 255, 255, 0.09);
  --ink-text:         #ffffff;
  --ink-text-muted:   rgba(255, 255, 255, 0.68);
  --ink-text-faint:   rgba(255, 255, 255, 0.46);
  --ink-border:       rgba(255, 255, 255, 0.13);
  --ink-border-strong:rgba(255, 255, 255, 0.24);

  /* The colour the curve is cut out in: the CONTENT background, not the band's.
     It is a separate token precisely because the band redeclares --color-bg,
     so the curve cannot read that and still know what it is opening onto. */
  --page-bg: #ffffff;

  /* Curve depth. Scales with the viewport and stops at both ends, so it is a
     detail on a phone and a statement on a desktop without ever dominating.

     This one number does two jobs, which is why it is the only thing that
     needed tuning. It is the height of the band the curve is drawn in, so it
     sets how pronounced the curve is; and the heroes reserve
     space-12 + this for their bottom padding, so it also sets how far the
     dark runs on past the last line of hero content.

     It came down from 4.5vw/68px, then again to 3.2vw/50px, and now to
     2.4vw/38px. On a compact hero — a resource page, where the title, blurb
     and download line stop early — 68px of curve sat under roughly 113px of
     mostly empty dark, and the shape stopped reading as an edge and started
     reading as a wave in an empty field. Lowering it lifts the transition and
     flattens the curve together, in proportion, which is the relationship
     that was wrong; scaling one without the other would only have traded one
     imbalance for another.

     WHERE THE FLOOR IS. This token can only ever account for its own height.
     The rest of the dark below the last line of hero content is space-12 —
     48px, the hero's own bottom spacing — and the padding is deliberately
     the SUM of the two so that content is always a full space-12 clear of the
     curve. So at 1440px this token now contributes 35px of the ~83px gap;
     closing the remaining 48px would mean changing the hero's spacing, which
     is a different decision from this one and not what is being asked here.
     Reducing this token much further would flatten the curve away to nothing
     while barely moving the boundary. */
  --ink-curve: clamp(18px, 2.4vw, 38px);

  /* THE SHAPE OF THE CURVE.
     A hand-drawn path rather than a radius, because the transition is meant to
     read as asymmetric and deliberate. It is used as a MASK, not as a picture:
     the fill is only an alpha channel, so the curve is still painted in
     --page-bg. Nothing here knows what colour it is.

     The path is the CONTENT side of the boundary — everything below the line,
     closed off along the bottom. Read left to right in its 1440x100 grid, four
     distinct movements, which is what stops it reading as one arc:

       0,53      the left edge leaves almost FLAT (its control sits at 52, a
                 single unit above the start) — the eye reads a straight edge
                 for the first stretch, then discovers it is falling
       ->560     the fall steepens through the first third and eases out again
       ~745,95   a long, shallow TROUGH rather than a point. Both of its
                 controls sit at 97, so the line lingers at the bottom instead
                 of turning a corner, and the low point lands at 52% — just
                 past centre, near enough to look intentional and far enough
                 that the halves can never look mirrored
       ->1440,23 a climb that keeps accelerating and is STILL rising as it
                 leaves the frame, which is what leaves the right edge sitting
                 30 units higher than the left

     Those end heights are the measurement that matters: the left edge sits
     about 42% of the way down the total travel, the right edge at the top of
     it. A symmetric shape puts both at the same height by definition, so this
     asymmetry is the whole point and is not something a radius can express.

     The joins are C1-continuous by construction — at 900,89 the tangent is
     (120,-8) on both sides — so the line is one unbroken stroke.

     The travel spans 72 of the 100 units rather than all of them: the curve
     should be a gesture across the width, not a wave. preserveAspectRatio
     ='none' lets the same path stretch to any width, which is what keeps one
     declaration correct from 320px to ultrawide. */
  --ink-curve-shape:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 100' preserveAspectRatio='none'%3E%3Cpath d='M0 53C190 52 300 82 560 91C720 97 780 97 900 89C1020 81 1240 55 1440 23L1440 100L0 100Z'/%3E%3C/svg%3E");
}

/* --------------------------------------------------------------------------
   The band itself.

   Listed explicitly rather than matched structurally: these are the five page
   openings the site actually has, and naming them means a component that is
   not part of the band can never accidentally be swallowed by it.
   -------------------------------------------------------------------------- */
.site-header,
.breadcrumbs,
.home-hero,
.category-hero,
.resource-hero,
.page-header,
.guide-hero,
.site-footer {
  --color-bg:            var(--ink-bg);
  --color-bg-elevated:   var(--ink-bg-elevated);
  --color-surface:       var(--ink-surface);
  --color-surface-hover: var(--ink-surface-hover);
  --color-surface-active:var(--ink-surface-hover);
  --color-text:          var(--ink-text);
  --color-text-muted:    var(--ink-text-muted);
  --color-text-faint:    var(--ink-text-faint);
  --color-border:        var(--ink-border);
  --color-border-strong: var(--ink-border-strong);
  --color-input-bg:      var(--ink-surface);
  --color-code-bg:       var(--ink-surface);
  --color-header-bg:     rgba(14, 12, 11, 0.86);

  background-color: var(--ink-bg);
  color: var(--ink-text);
}

/* The nav sits on top of the hero, so its own divider would draw a line
   through the middle of one continuous black field. */
.site-header {
  border-bottom-color: transparent;
}

/* FORM CONTROLS KEEP THEIR OWN COLOURS INSIDE THE BAND.
   The band redeclares --color-text to white so that text on the black ground
   is readable. An <input> inside it, however, paints its own light background
   from a component rule — so it inherited white text onto a white field and
   what the visitor typed became invisible. That is exactly what happened to the
   query on /search/: the heading said Results for "shader" above an apparently
   empty box.
   Inputs therefore opt out of the inherited colour and state both halves. */
.site-header input,
.breadcrumbs input,
.home-hero input,
.category-hero input,
.resource-hero input,
.page-header input,
.guide-hero input,
.site-footer input,
.site-header textarea,
.page-header textarea {
  color: #201c18;
  background-color: #ffffff;
  /* The border must read against the black ground, not against the field. */
  border-color: var(--ink-border-strong);
}

.site-header input::placeholder,
.page-header input::placeholder {
  color: #5a5450;
  opacity: 1;
}

.breadcrumbs {
  border-bottom: 0;
}

/* --------------------------------------------------------------------------
   The curve.

   A band of the CONTENT colour laid over the bottom edge of the hero and cut
   to shape by --ink-curve-shape. The content area appears to rise into the
   dark rather than the dark being cut away, which keeps the join seamless at
   every width.

   WHY A MASK AND NOT A BORDER-RADIUS. This was an elliptical dome —
   `border-radius: 50% 50% 0 0 / 100% 100% 0 0` — which is symmetric by
   definition and, more to the point, curved the wrong way: the light side
   bulged UP through the middle and the dark reached lowest at the two corners.
   A radius cannot express the intended line, which falls through the middle,
   is deepest right of centre, and leaves the two edges at different heights.
   No radius, and no combination of them, is asymmetric across its own axis.
   The path is therefore the shape, and this element is only the material it is
   cut from.

   WHY NOT clip-path: path(). Its coordinates are absolute pixels with no
   viewBox, so it cannot stretch to the element — it would need a different
   path per breakpoint. An SVG mask with preserveAspectRatio='none' scales to
   whatever box it is given, which is why one declaration covers every width.

   WITHOUT MASK SUPPORT the element stays a plain rectangle of --page-bg: the
   hero simply ends on a straight edge, exactly as it did before this section
   existed. It degrades to flat, never to the wrong curve, and never to a gap.

   inset-inline: 0 is load-bearing. An earlier approach bled the shape past the
   viewport edges with negative offsets to flatten the curve, which put a
   horizontal scrollbar on every page. Staying inside the element cannot
   overflow, at any width, in any writing mode.
   -------------------------------------------------------------------------- */
/* THE DOUBLED CLASS IS DELIBERATE, and it is load-bearing.
   .home-hero and .category-hero declare `padding-block` in components.css,
   which the browser loads AFTER this file. At equal specificity the later
   sheet wins, so the single-class version of this rule was discarded and the
   hero kept its original bottom padding — leaving the curve to be drawn
   directly over the last row of hero content. On the homepage that clipped the
   three feature captions in half.
   Repeating the class raises specificity just enough to win from here, without
   moving this section into a component sheet it does not belong to, and
   without !important. Breakpoint overrides in responsive.css still win,
   because none of them touches these paddings. */
.home-hero.home-hero,
.category-hero.category-hero,
.resource-hero.resource-hero,
.page-header.page-header,
.guide-hero.guide-hero {
  position: relative;
  /* Room for the curve to occupy without eating into the hero's content. */
  padding-bottom: calc(var(--space-12) + var(--ink-curve));
  /* The curve replaces the rule that used to separate hero from content. */
  border-bottom: 0;
}

.home-hero::after,
.category-hero::after,
.resource-hero::after,
.page-header::after,
.guide-hero::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  /* -1px closes the hairline some browsers leave between a shaped fill and the
     box below it at fractional device pixel ratios. The path is closed flat
     along its own bottom edge, so this overlap is solid colour over the same
     solid colour and cannot show as a seam. */
  bottom: -1px;
  height: var(--ink-curve);
  background-color: var(--page-bg);
  /* Stretched to the element, never tiled: the shape must span the full width
     once, whatever that width is. */
  -webkit-mask-image: var(--ink-curve-shape);
          mask-image: var(--ink-curve-shape);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center bottom;
          mask-position: center bottom;
  pointer-events: none;
}

/* The resource hero draws its own divider on an inner element, declared in
   components.css — so this needs the same specificity bump to reach it. */
.resource-hero.resource-hero .container-wide {
  border-bottom: 0;
}

/* Anything positioned inside a hero must stay above the curve. */
.home-hero > *,
.category-hero > *,
.resource-hero > *,
.page-header > *,
.guide-hero > * {
  position: relative;
  z-index: 1;
}

/* --------------------------------------------------------------------------
   The footer closes the page with a hard edge, not a curve: the curve is the
   moment the page opens up, and repeating it at the bottom would read as
   decoration rather than structure.
   -------------------------------------------------------------------------- */
.site-footer {
  border-top: 0;
}

/* --------------------------------------------------------------------------
   The guide hero.

   CMS guide pages had no full-width opening at all — the article began
   directly under the breadcrumbs — so there was nothing for the band to end
   on. bravith_render_guide_page() now emits this wrapper, which gives a guide
   the same opening every other page type has.
   -------------------------------------------------------------------------- */
.guide-hero {
  padding-top: var(--space-6);
}

/* The width and gutters now belong to .guide-hero-inner, which is a
   .container like every other full-width block on the site — so a guide hero
   lines up with the nav, the breadcrumb and the article below it instead of
   sitting in a measure of its own. The copy column keeps only what is its own
   business: no border, and a cap so a long lede does not run the full width of
   a desktop hero. */
.guide-hero .guide-header {
  border-bottom: 0;
  max-width: 68ch;
}

/* Fluid rather than two fixed sizes. A guide title is content — it can be
   three words or twelve — so it scales with the viewport between a floor and a
   ceiling and never needs a breakpoint per title length. */
.guide-hero .guide-header h1 {
  font-size: clamp(2rem, 1.35rem + 2.1vw, 3rem);
  line-height: 1.12;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

/* --------------------------------------------------------------------------
   Accessibility and motion.

   The band raises contrast rather than lowering it (white on #0e0c0b is about
   19:1, the muted tone about 12:1), so nothing here needs a forced-colors
   exception. In forced-colors mode the curve is dropped entirely: it is a
   decorative fill, and the system palette should own the page.
   -------------------------------------------------------------------------- */
@media (forced-colors: active) {
  .home-hero::after,
  .category-hero::after,
  .resource-hero::after,
  .page-header::after,
  .guide-hero::after {
    display: none;
  }

  .site-header,
  .breadcrumbs,
  .home-hero,
  .category-hero,
  .resource-hero,
  .page-header,
  .guide-hero,
  .site-footer {
    background-color: Canvas;
    color: CanvasText;
  }
}