/* ==================================================================
   Frostpane — Klondaik edition

   Based on Frostpane by KrekerDM (MIT). The original is a dark-only
   system: white frosted panes over a photograph, no borders, square
   corners, one accent.

   Two things were added here, and each one is a decision worth
   knowing about:

   1. The accent became a variable. Klondaik lets the reader pick one
      of five. A fixed hex per role would mean 5 x 2 themes x 3 roles =
      30 values to keep in contrast. Instead the accent is one hue and
      one chroma, and each role is a lightness in OKLCH. OKLCH is used
      precisely because its L tracks perceived lightness, so the same
      L lands at roughly the same contrast on every hue.

   2. The fill carries white text, not dark. This is a deliberate break
      from the original, which puts dark text on a pale accent. Klondaik's
      accent is a saturated brand colour and going pale enough for dark
      text drains it. So the fill sits at L 0.54 / 0.52 and the text on
      it is white.

   All of it is measured, not guessed: tools/contrast.py composites the
   image, the veil and the pane tint in the same order the CSS does,
   and reports the worst case for every token on every accent. Worst
   case today is 4.68:1. AA is 4.5:1. Swap the backdrop and that number
   no longer applies — run it again.

   There is one theme. A light palette used to live here and was
   removed on purpose: this catalogue is read on a dark ground and the
   second palette was carrying its own veil, its own backdrop filter
   and its own contrast budget for nobody.
   ================================================================== */

/* ------------------------------------------------------------------
   1. Tokens

   Graphite, not navy. The first version of this file was a blue-black
   ground with a saturated crimson accent, which is the single most
   recognisable "dark theme" cliche there is, and it made the page look
   generated. Warm charcoal with a muted accent reads as a catalogue
   printed on dark paper instead.
   ------------------------------------------------------------------ */
:root {
  color-scheme: dark;

  /* Warm near-black. Slightly off-neutral so whites on top read warm
     rather than clinical. */
  --ground: #171613;

  /* The photograph is desaturated and LIFTED, not darkened. At its own
     exposure this backdrop is almost black (median luminance 3/255), so
     darkening it further leaves nothing at all; grayscale plus a x2
     brightness turns it into a faint grey texture - material under the
     page rather than a picture competing with it. */
  --backdrop-filter: grayscale(0.9) brightness(2);

  --veil: linear-gradient(
    to bottom,
    rgba(23, 22, 19, 0.55) 0%,
    rgba(23, 22, 19, 0.58) 45%,
    rgba(23, 22, 19, 0.62) 100%
  );
  --vignette: radial-gradient(
    ellipse 95% 70% at 50% 28%,
    transparent 0%,
    rgba(10, 9, 8, 0.2) 68%,
    rgba(10, 9, 8, 0.46) 100%
  );

  --surface: rgba(255, 255, 255, 0.07);
  --surface-blur: blur(50px) saturate(120%);
  --surface-shadow: 0 18px 40px -22px rgba(0, 0, 0, 0.7);

  /* Cards are a step LIGHTER than what is behind them, not darker.
     Frostpane's rule - a card inside a pane is darker than the pane -
     is about a card nested in a surface. These sit straight on the
     backdrop, and copying the rule here measured 1.01:1 against the
     page: the cards had no edge at all. */
  --card: rgba(255, 255, 255, 0.055);
  /* On a near-black ground no tint alone can draw an edge: white at 9%
     still only reaches 1.26:1. The line is what makes the boundary
     visible, at 1.38:1 against the card and 1.54:1 against the page. */
  --card-edge: rgba(255, 255, 255, 0.11);

  /* The same card with no line at all. A drawn edge and a tint are two
     ways to say the same thing, and the line was the more insistent of
     the two - ninety of them in a grid read as a table.
     Taking it away means the fill has to carry the whole separation,
     so this is not --card: at 0.055 a borderless card measures 1.17:1
     against the page, which is the 1.01:1 problem coming back. 0.11
     measures 1.41:1 - the same step the line was drawing, now as
     material rather than as an outline. */
  --card-plain: rgba(255, 255, 255, 0.11);
  --hover: rgba(255, 255, 255, 0.1);
  --hairline: rgba(255, 255, 255, 0.12);

  /* Warm whites, to sit with graphite instead of fighting it. */
  --text: #f5f3ef;
  --text-soft: #e6e2db;
  --text-dim: #e2ded6;

  --btn-ghost: rgba(255, 255, 255, 0.06);
  --btn-ghost-border: rgba(255, 255, 255, 0.18);
  --btn-ghost-hover: rgba(255, 255, 255, 0.12);

  --nav: linear-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.05)),
    rgba(20, 19, 16, 0.55);
  --nav-blur: blur(50px) saturate(120%);

  --field: rgba(0, 0, 0, 0.28);
  --field-border: rgba(255, 255, 255, 0.14);

  /* The accent, by role.
     --accent-c-fill pulls the chroma down hard here. The picker offers
     five saturated brand hues, and at full strength any of them on
     graphite is the neon-pop-on-black look this theme exists to avoid.
     Halved, they read as a tinted material.
     The fill is light with dark text on it, which is quieter than a
     deep fill with white text and is the original frostpane
     relationship. */
  --accent-l-fill: 0.74;
  --accent-l-hover: 0.82;
  --accent-l-text: 0.9;
  --accent-c-fill: 0.42;
  --accent-c-text: 0.38;
  --on-accent: #17150f;
}

/* ------------------------------------------------------------------
   3. The accent

   One hue and one chroma per choice. Everything else is derived, so
   adding a sixth accent means adding two numbers, not six colours.
   Values are the OKLCH coordinates of the original brand hexes.
   ------------------------------------------------------------------ */
:root,
:root[data-accent="rose"] {
  --accent-h: 17.6;
  --accent-c: 0.222;
}
:root[data-accent="blue"] {
  --accent-h: 259.8;
  --accent-c: 0.188;
}
:root[data-accent="emerald"] {
  --accent-h: 162.5;
  --accent-c: 0.149;
}
:root[data-accent="purple"] {
  --accent-h: 303.9;
  --accent-c: 0.2325;
}
:root[data-accent="amber"] {
  --accent-h: 70.1;
  --accent-c: 0.1647;
}

:root {
  /* The fill: buttons, markers, the focus ring. Chroma is scaled per
     theme, so the same five hues can be vivid on white and muted on
     graphite without a second palette. */
  --accent: oklch(
    var(--accent-l-fill) calc(var(--accent-c) * var(--accent-c-fill)) var(--accent-h)
  );
  --accent-bright: oklch(
    var(--accent-l-hover) calc(var(--accent-c) * var(--accent-c-fill)) var(--accent-h)
  );
  /* The accent as text on a glass pane. One value cannot do both jobs
     and still clear AA — the fill colour used as link text measures
     around 3:1 in the original. */
  --accent-text: oklch(
    var(--accent-l-text) calc(var(--accent-c) * var(--accent-c-text))
      var(--accent-h)
  );
  --accent-soft: color-mix(in srgb, var(--accent) 16%, transparent);
  --accent-line: color-mix(in srgb, var(--accent) 42%, transparent);
}

/* ------------------------------------------------------------------
   4. The rest of the system — theme-independent
   ------------------------------------------------------------------ */
:root {
  --font: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "Cascadia Code", monospace;
  --font-mark: "Press Start 2P", var(--font-mono), monospace;

  /* Fixed steps, not fluid. A clamp() scale produces in-between sizes
     that exist nowhere else, and then two headings stop matching. */
  --t-h1: 3rem;
  --t-h2: 2.25rem;
  --t-h3: 1.5rem;
  --t-lead: 1.125rem;
  --t-body: 1rem;
  --t-small: 0.875rem;
  --t-label: 0.8125rem;

  /* Every value is a multiple of 4px. If you need 14, take 16. */
  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-6: 1.5rem;
  --s-8: 2rem;
  --s-10: 2.5rem;
  --s-16: 4rem;

  --wrap: 64rem;

  /* One curve for everything that moves. It eases out at the end
     instead of stopping dead. */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
}

@media (max-width: 640px) {
  :root {
    --t-h1: 2.25rem;
    --t-h2: 1.75rem;
    --t-h3: 1.25rem;
  }
}

/* ------------------------------------------------------------------
   5. Base
   ------------------------------------------------------------------ */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--accent) 35%, transparent) transparent;
}

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

body {
  margin: 0;
  background-color: var(--ground);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: clip;
}

img,
video {
  display: block;
  max-width: 100%;
  height: auto;
}

/* An author rule like .bento-grid { display: grid } outranks the UA
   sheet's [hidden] { display: none }, so hidden elements would stay on
   screen. This puts it back, once, for the whole system. */
[hidden] {
  display: none !important;
}

a {
  color: inherit;
}

::selection {
  background-color: color-mix(in srgb, var(--accent) 30%, transparent);
}

/* The focus ring is the accent, not the browser default, and it sits
   outside the element so it stays visible on glass. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--accent) 30%, transparent);
  border: 3px solid transparent;
  background-clip: padding-box;
}

/* ------------------------------------------------------------------
   6. Backdrop

   The photograph is fixed behind everything. Tone-map it first: lift
   the shadows, put a knee on the highlights, or the glass on top turns
   dark and muddy in one corner and washed out in the other.
   ------------------------------------------------------------------ */
.backdrop {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-color: var(--ground);
}
.backdrop::before,
.backdrop::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* The image lives on its own layer because of --backdrop-filter: a
   filter on .backdrop itself would invert the veil and the vignette
   along with the photograph, which is the opposite of what either is
   for. Veil and vignette therefore share the second layer. */
.backdrop::before {
  background-image: var(--backdrop-image);
  background-size: cover;
  background-position: top center;
  background-repeat: no-repeat;
  filter: var(--backdrop-filter);
}
.backdrop::after {
  background: var(--vignette), var(--veil);
}

/* Content has to sit above the backdrop. */
.above {
  position: relative;
  z-index: 10;
}

/* ------------------------------------------------------------------
   7. Surfaces

   Square corners. That is the choice the whole look rests on, and the
   first thing to disappear the moment someone softens it up a bit.
   ------------------------------------------------------------------ */
.pane {
  position: relative;
  border-radius: 0;
  background: var(--surface);
  backdrop-filter: var(--surface-blur);
  -webkit-backdrop-filter: var(--surface-blur);
  box-shadow: var(--surface-shadow);
}

/* Inside a .pane. No blur: backdrop-filter within backdrop-filter adds
   no effect, only another layer to paint. This is also what keeps a
   grid of ninety cards from crawling — see the note in the README. */
/* A .pane is separated from the page by its shadow. A .pane-solid has
   neither blur nor shadow, so on a dark ground its edge has to be
   drawn or it does not exist - which is exactly what happened to the
   catalogue cards in the first version. */
.pane-solid {
  position: relative;
  border-radius: 0;
  background: var(--card);
  border: 1px solid var(--card-edge);
}

.pane-link {
  display: block;
  text-decoration: none;
  transition:
    background-color 0.3s var(--ease),
    box-shadow 0.3s var(--ease);
}
.pane-link:hover {
  background-color: var(--hover);
}

.nav-glass {
  background: var(--nav);
  backdrop-filter: var(--nav-blur);
  -webkit-backdrop-filter: var(--nav-blur);
}

.rule-t {
  border-top: 1px solid var(--hairline);
}

/* ------------------------------------------------------------------
   8. Layout
   ------------------------------------------------------------------ */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--s-6);
}

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

.stack > * + * {
  margin-top: var(--s-4);
}
.stack-lg > * + * {
  margin-top: var(--s-8);
}

/* ------------------------------------------------------------------
   9. Type
   ------------------------------------------------------------------ */
.h1,
.h2,
.h3 {
  margin: 0;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.h1 {
  font-size: var(--t-h1);
}
.h2 {
  font-size: var(--t-h2);
}
.h3 {
  font-size: var(--t-h3);
  line-height: 1.25;
}

.lead {
  font-size: var(--t-lead);
  line-height: 1.6;
  color: var(--text-soft);
  text-wrap: pretty;
}

.prose {
  max-width: 65ch;
  color: var(--text-soft);
  text-wrap: pretty;
}
.prose-dim {
  color: var(--text-dim);
}

.label {
  font-size: var(--t-label);
  font-weight: 500;
  color: var(--text-dim);
}

.link {
  color: var(--text);
  text-decoration: underline;
  text-decoration-color: var(--hairline);
  text-underline-offset: 4px;
  transition: color 0.25s var(--ease);
}
.link:hover {
  color: var(--accent-text);
  text-decoration-color: var(--accent-line);
}

/* Text sitting straight on the photograph with no pane under it. The
   shadow is for the frames where the image is brightest right behind
   the letters. In light the shadow would only smear, so it goes. */
.on-photo {
  text-shadow:
    0 2px 18px rgba(6, 9, 16, 0.75),
    0 1px 3px rgba(6, 9, 16, 0.6);
}

.mono {
  font-family: var(--font-mono);
  font-size: var(--t-small);
}

/* ------------------------------------------------------------------
   10. Buttons

   One primary button per screen. Two equally heavy buttons side by
   side usually means nobody decided what the reader is supposed to do.
   ------------------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 0.8rem 1.4rem;
  border: 1px solid transparent;
  border-radius: 0;
  font: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color 0.25s var(--ease),
    border-color 0.25s var(--ease),
    color 0.25s var(--ease);
}

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.btn-primary:hover {
  background: var(--accent-bright);
  border-color: var(--accent-bright);
}

.btn-ghost {
  background: var(--btn-ghost);
  border-color: var(--btn-ghost-border);
  color: var(--text);
}
.btn-ghost:hover {
  background: var(--btn-ghost-hover);
}

/* ------------------------------------------------------------------
   11. Motion

   Two rules:
   1. The resting state is always the visible one. The animation takes
      away something already there, so the page reads even if the
      script never runs.
   2. Everything respects prefers-reduced-motion.

   Rule 1 is not theoretical. With an animation library and reduced
   motion on, a whole page once sat at opacity 0, because the animation
   meant to set it back to 1 never ran.
   ------------------------------------------------------------------ */
@keyframes page-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Do not put this on an ancestor of anything position: fixed. It
   animates transform, and an element with a transform animation is a
   containing block for fixed descendants even while the computed value
   is none - so a fixed sidebar inside it stops being fixed and scrolls
   away with the page. Put it on the scrolling content instead. */
.page-in {
  animation: page-in 0.5s var(--ease) forwards;
}

/* Visible to begin with. JS sets data-armed="true" only once it knows
   it can fade the content back in. */
.reveal {
  opacity: 1;
}
.reveal[data-armed="true"] {
  opacity: 0;
  transform: translateY(18px);
  transition:
    opacity 0.7s var(--ease),
    transform 0.7s var(--ease);
}
.reveal[data-armed="true"][data-visible="true"] {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .reveal[data-armed="true"] {
    opacity: 1;
    transform: none;
  }
}

/* ------------------------------------------------------------------
   12. Odds and ends
   ------------------------------------------------------------------ */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.text-balance {
  text-wrap: balance;
}
.text-pretty {
  text-wrap: pretty;
}
