/* =========================================================================
   lets-python — colour tokens, typography, and the motion layer.

   dwind handles layout (flex/grid/gap/padding/responsive). This file owns
   colour, type scale, and motion — the three things that are far more
   readable as plain CSS than as macro strings, and the three things the
   Apple-style feel actually depends on.

   Theming is one mechanism: custom properties swapped by `.light` on
   <html>. The inline script in index.html sets that class before first
   paint; app.rs toggles it thereafter.
   ========================================================================= */

:root {
  /* Dark is the default. */
  --bg: #08080b;
  --bg-raised: #101015;
  --bg-sunken: #050507;
  --surface: rgba(255, 255, 255, 0.035);
  --surface-strong: rgba(255, 255, 255, 0.06);

  /* Every foreground here clears WCAG AA (4.5:1) against both --bg and the
     darker --bg-sunken that code blocks sit on. `--text-faint` in particular
     is not decorative — it carries the inactive nav labels and code comments. */
  --text: #f5f5f7;
  --text-muted: #a1a1aa;
  --text-faint: #83838f;

  --rule: rgba(255, 255, 255, 0.09);
  --rule-strong: rgba(255, 255, 255, 0.16);

  --accent: #4da3ff;
  --accent-dim: #2b6fc4;
  --accent-wash: rgba(77, 163, 255, 0.12);

  /* Python yellow — reserved for the "why" callouts so they read as a
     distinct voice from the surrounding prose. */
  --why: #ffd43b;
  --why-wash: rgba(255, 212, 59, 0.09);
  --why-rule: rgba(255, 212, 59, 0.32);

  --good: #4ade80;
  --bad: #f87171;

  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.35);

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --header-h: 3.75rem;

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter",
    "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo,
    Consolas, "Liberation Mono", monospace;
}

html.light {
  --bg: #fbfbfd;
  --bg-raised: #ffffff;
  --bg-sunken: #f0f0f3;
  --surface: rgba(0, 0, 0, 0.028);
  --surface-strong: rgba(0, 0, 0, 0.05);

  --text: #1d1d1f;
  --text-muted: #52525b;
  --text-faint: #68686d;

  --rule: rgba(0, 0, 0, 0.1);
  --rule-strong: rgba(0, 0, 0, 0.18);

  --accent: #0066cc;
  --accent-dim: #4a9eea;
  --accent-wash: rgba(0, 102, 204, 0.08);

  /* Measured against the composited wash it sits on, not against --bg. */
  --why: #8a6000;
  --why-wash: rgba(255, 196, 0, 0.11);
  --why-rule: rgba(154, 107, 0, 0.28);

  /* Darker than the usual green-500: this also colours string literals inside
     code blocks, where the lighter shade fell to 2.9:1. */
  --good: #136c34;
  --bad: #b91c1c;

  --shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 8px 28px rgba(0, 0, 0, 0.08);
}

/* -------------------------------------------------------------------------
   Base
   ------------------------------------------------------------------------- */

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* The sticky header would otherwise sit on top of a section heading that
     the browser just scrolled to.
     This is the *only* place that offset is applied. Adding `scroll-margin-top`
     to the sections as well does not override it — the two stack, and every
     anchor lands one header-height too low. */
  scroll-padding-top: calc(var(--header-h) + 1.5rem);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 1.0625rem;
  line-height: 1.7;
  letter-spacing: -0.011em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Colour is the one thing that should cross-fade on a theme flip. */
  transition: background-color 0.35s var(--ease), color 0.35s var(--ease);
}

/* Nothing may push the page sideways — the single most common way a
   responsive page breaks on a phone. */
html,
body {
  overflow-x: clip;
}

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

a:hover {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

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

::selection {
  background: var(--accent);
  color: #fff;
}

/* -------------------------------------------------------------------------
   Type scale — fluid, so it reads well from 360px to an ultrawide without
   a pile of breakpoints.
   ------------------------------------------------------------------------- */

.t-hero {
  font-size: clamp(2.75rem, 7.5vw, 5.5rem);
  line-height: 1.04;
  letter-spacing: -0.035em;
  font-weight: 700;
  margin: 0;
}

.t-lede {
  font-size: clamp(1.1rem, 2.2vw, 1.5rem);
  line-height: 1.5;
  letter-spacing: -0.018em;
  color: var(--text-muted);
  font-weight: 400;
  margin: 0;
}

.t-h2 {
  font-size: clamp(1.85rem, 4vw, 2.85rem);
  line-height: 1.12;
  letter-spacing: -0.028em;
  font-weight: 700;
  margin: 0;
}

.t-h3 {
  font-size: clamp(1.1rem, 2vw, 1.3rem);
  line-height: 1.35;
  letter-spacing: -0.018em;
  font-weight: 600;
  margin: 0;
}

.t-body {
  font-size: 1.0625rem;
  line-height: 1.75;
  color: var(--text-muted);
  margin: 0;
  max-width: 68ch;
}

.t-small {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--text-faint);
}

.t-eyebrow {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--accent);
}

.t-strong {
  color: var(--text);
  font-weight: 600;
}

.bullets {
  list-style: disc;
  padding-left: 1.3rem;
  margin: 0;
}

.bullets li + li {
  margin-top: 0.5rem;
}

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

/* Vertical rhythm between sections. In the guide this is generous; the deck
   overrides it, which is only possible because it is a class rather than an
   inline style on the element. */
.guide-section {
  padding-block: clamp(3.5rem, 9vw, 7rem);
}

/* -------------------------------------------------------------------------
   Surfaces
   ------------------------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 14px;
  box-shadow: var(--shadow);
}

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

/* Frosted header. backdrop-filter is a progressive nicety — without it the
   fallback background is already opaque enough to read against. */
.chrome {
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  border-bottom: 1px solid var(--rule);
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .chrome {
    background: color-mix(in srgb, var(--bg) 68%, transparent);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
  }
}

/* -------------------------------------------------------------------------
   Code
   ------------------------------------------------------------------------- */

.code {
  --code-line: 1.75;

  font-family: var(--font-mono);
  font-size: 0.9rem;
  line-height: var(--code-line);
  background: var(--bg-sunken);
  border: 1px solid var(--rule);
  border-radius: 12px;
  /* Long lines scroll inside the block; they never widen the page. */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.code pre {
  margin: 0;
  padding: 1rem 1.15rem;
  white-space: pre;
  min-width: max-content;
}

/* Each source line is its own div, so a blank line is an *empty* div — and an
   empty div is zero pixels tall. Without this, every blank line separating the
   tables in a pyproject.toml example silently collapses and the block reads as
   one undifferentiated wall.

   Tied to --code-line so it is exactly one line box: identical to the natural
   height of a line with content, so nothing else shifts. */
.code pre > div {
  min-height: calc(var(--code-line) * 1em);
}

.code-label {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.02em;
  color: var(--text-faint);
  padding: 0.5rem 1.15rem;
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
}

.tok-key {
  color: var(--accent);
}

.tok-com {
  color: var(--text-faint);
  font-style: italic;
}

.tok-str {
  color: var(--good);
}

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

code.inline {
  font-family: var(--font-mono);
  font-size: 0.88em;
  background: var(--surface-strong);
  border: 1px solid var(--rule);
  border-radius: 5px;
  padding: 0.1em 0.36em;
  color: var(--text);
  white-space: nowrap;
}

/* -------------------------------------------------------------------------
   "Why" callout — the whole point of the guide, so it gets its own voice.
   ------------------------------------------------------------------------- */

.why {
  background: var(--why-wash);
  border-left: 3px solid var(--why-rule);
  border-radius: 0 12px 12px 0;
  padding: 1rem 1.25rem;
}

.why-label {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--why);
}

/* -------------------------------------------------------------------------
   Page layout

   The sidebar/pill-bar swap lives here rather than in dwind's responsive
   prefixes so that a single stylesheet owns the breakpoint — two mechanisms
   both setting `display` is a fight waiting to happen.
   ------------------------------------------------------------------------- */

.page {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
}

.header-bar {
  height: var(--header-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-inline: clamp(1rem, 4vw, 2.5rem);
  max-width: 82rem;
  margin-inline: auto;
  width: 100%;
}

.brand {
  font-family: var(--font-mono);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text);
}

.brand:hover {
  text-decoration: none;
  color: var(--accent);
}

main {
  flex: 1;
}

.shell {
  display: flex;
  gap: clamp(1.5rem, 4vw, 4rem);
  align-items: flex-start;
  max-width: 82rem;
  margin-inline: auto;
  padding-inline: clamp(1rem, 4vw, 2.5rem);
  width: 100%;
}

.content {
  flex: 1 1 auto;
  /* Without this a wide code block can force the flex item past its share and
     push the whole page sideways. */
  min-width: 0;
}

.content > section + section {
  border-top: 1px solid var(--rule);
}

.nav-side {
  display: none;
  flex-direction: column;
  gap: 0.1rem;
  position: sticky;
  top: calc(var(--header-h) + 2rem);
  width: 14rem;
  flex: 0 0 14rem;
  align-self: flex-start;
  border-left: 1px solid var(--rule);
  /* A long list must scroll inside itself rather than run off the viewport. */
  max-height: calc(100vh - var(--header-h) - 4rem);
  overflow-y: auto;
}

.nav-pills {
  display: flex;
  padding: 0.5rem 1rem;
  border-top: 1px solid var(--rule);
}

@media (min-width: 1080px) {
  .nav-side {
    display: flex;
  }

  .nav-pills {
    display: none;
  }
}

.site-footer {
  border-top: 1px solid var(--rule);
  padding: 2.5rem clamp(1rem, 4vw, 2.5rem);
  max-width: 82rem;
  margin-inline: auto;
  width: 100%;
}

/* -------------------------------------------------------------------------
   Hero — full-bleed, above the two-column area.
   ------------------------------------------------------------------------- */

.hero {
  padding-block: clamp(4rem, 14vh, 9rem) clamp(3rem, 10vh, 6rem);
  padding-inline: clamp(1rem, 4vw, 2.5rem);
  max-width: 82rem;
  margin-inline: auto;
  width: 100%;
}

.hero-grid {
  display: flex;
  flex-direction: column;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: stretch;
}

.hero-inner {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.hero-figure {
  min-width: 0;
}

@media (min-width: 1080px) {
  .hero-grid {
    flex-direction: row;
    align-items: center;
  }

  .hero-inner {
    flex: 1 1 55%;
    min-width: 0;
  }

  .hero-figure {
    flex: 1 1 45%;
  }
}

/* Capped in `ch` of the hero's own size, so the headline breaks into the same
   shape whatever the viewport does to `clamp()`. */
.hero .t-hero {
  max-width: 16ch;
}

/* The hero's terminal is decoration, not a section's code sample — it can
   afford to be a little more present. */
.hero-figure .code {
  font-size: 0.95rem;
  box-shadow: var(--shadow);
}

.hero-hint {
  margin-top: 1rem;
  color: var(--text-faint);
}

/* -------------------------------------------------------------------------
   Nav
   ------------------------------------------------------------------------- */

.navlink {
  display: block;
  padding: 0.34rem 0 0.34rem 1rem;
  color: var(--text-faint);
  font-size: 0.9rem;
  line-height: 1.45;
  border-left: 2px solid transparent;
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease);
}

.navlink:hover {
  color: var(--text);
  text-decoration: none;
}

.navlink.active {
  color: var(--accent);
  border-left-color: var(--accent);
  font-weight: 600;
}

/* Mobile: the same list as a horizontally scrolling pill bar.
   `display` deliberately lives on `.nav-pills` in the layout section above,
   never here — this rule is further down the file, so setting it here would
   quietly beat the `min-width: 1080px` media query that hides the bar. */
.pillbar {
  gap: 0.5rem;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  scroll-padding-inline: 1rem;
}

.pillbar::-webkit-scrollbar {
  display: none;
}

.pill {
  flex: 0 0 auto;
  padding: 0.3rem 0.8rem;
  border-radius: 999px;
  font-size: 0.83rem;
  white-space: nowrap;
  color: var(--text-muted);
  background: var(--surface);
  border: 1px solid var(--rule);
  transition: color 0.25s var(--ease), background-color 0.25s var(--ease),
    border-color 0.25s var(--ease);
}

.pill:hover {
  text-decoration: none;
  color: var(--text);
}

.pill.active {
  color: var(--bg);
  background: var(--accent);
  border-color: var(--accent);
  font-weight: 600;
}

.iconbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.25rem;
  height: 2.25rem;
  padding: 0 0.6rem;
  border-radius: 999px;
  border: 1px solid var(--rule);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.2s var(--ease), border-color 0.2s var(--ease),
    transform 0.15s var(--ease);
}

.iconbtn:hover {
  background: var(--surface-strong);
  border-color: var(--rule-strong);
}

.iconbtn:active {
  transform: scale(0.94);
}

/* -------------------------------------------------------------------------
   Two-column sections, and the sticky visual — the core Apple move: the
   figure pins while the prose beside it scrolls past.

   The `align-items` value has to differ per direction, which is why this is
   a media query rather than an inline style:

   - Row: `flex-start` so a column is only as tall as its content, which is
     what gives `position: sticky` room to travel. Stretched to full height,
     it has nowhere to move and the pin silently does nothing.
   - Column: `stretch`, so children take the container's width. With
     `flex-start` they size to max-content instead — and a code block, whose
     `pre` is deliberately `min-width: max-content`, then grows to its widest
     line and pushes straight off a phone screen instead of scrolling inside
     its own box.
   ------------------------------------------------------------------------- */

.split {
  display: flex;
  flex-direction: column;
  gap: 2rem;
  align-items: stretch;
}

.split > * {
  flex: 1 1 0;
  /* A flex item's default `min-width: auto` refuses to shrink below its
     content — the other half of the same overflow story. */
  min-width: 0;
}

/* Before/after pairs. A grid rather than a flex row, because the two halves
   have to keep the same width whatever is inside them — a four-line "after"
   next to a ten-line "before" should still read as a comparison, not as a
   wide column and a narrow one.

   Deliberately the same breakpoint and gaps as `.split` above: a section that
   uses both should change shape in one step. */
.duo {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 2rem;
  align-items: start;
}

/* `minmax(0, ...)` handles the track; this handles the item inside it. Both
   are needed, or a code block's `min-width: max-content` wins and the page
   scrolls sideways instead of the block doing it. */
.duo > * {
  min-width: 0;
}

@media (min-width: 1280px) {
  .split {
    flex-direction: row;
    gap: 3rem;
    align-items: flex-start;
  }

  .duo {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 3rem;
  }

  .pin {
    position: sticky;
    top: calc(var(--header-h) + 2.5rem);
  }
}

/* =========================================================================
   PRESENTATION MODE

   Same DOM, one section at a time. Everything here is driven by a single
   `present` class on <html>, so switching modes is a class toggle rather
   than a rebuild — instant, and there is only one copy of the content.

   The slide bar is in the tree in both modes and simply hidden while
   reading, which keeps its state (position, progress) alive across a
   detour into the deck and back.
   ========================================================================= */

.slide-bar {
  display: none;
}

html.present {
  /* The deck owns the viewport; the page itself never scrolls. A slide too
     tall for the screen scrolls inside itself instead — see below. */
  overflow: hidden;
}

html.present body {
  overflow: hidden;
  height: 100vh;
}

/* Reading-mode furniture that a deck has no use for. */
html.present .nav-side,
html.present .nav-pills,
html.present .site-footer,
html.present .hero-hint {
  display: none;
}

html.present .page {
  height: 100vh;
}

html.present main {
  height: calc(100vh - var(--header-h));
  overflow: hidden;
}

html.present .shell {
  max-width: 100%;
  padding-inline: 0;
  height: 100%;
}

html.present .content {
  height: 100%;
}

/* One slide visible. Sections keep their ids and their content — only their
   visibility changes. */
html.present section {
  display: none;
}

html.present section.slide-current {
  display: block;
  height: 100%;
  /* Dense slides (three code samples side by side) scroll rather than being
     clipped. reset_slide_scroll() returns each to its top on arrival. */
  overflow-y: auto;
  /* Tighter than the guide's rhythm — a slide has one screen to work with,
     and reading-mode whitespace here just pushes content under the fold. */
  padding: clamp(1rem, 2.5vh, 2rem) clamp(1.5rem, 5vw, 5rem) 5rem;
  max-width: 90rem;
  margin-inline: auto;
  animation: slide-in 0.4s var(--ease) both;
}

html.present .content > section + section {
  border-top: none;
}

/* The hero is a section like any other, so it becomes slide 1. */
html.present .hero.slide-current {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: 0;
}

@keyframes slide-in {
  from {
    opacity: 0;
    transform: translateY(1rem);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Projector legibility: a slide is read from the back of a room, not from
   40cm away. */
html.present .t-h2 {
  font-size: clamp(2rem, 3.6vw, 3.2rem);
}

html.present .t-body,
html.present .why p {
  font-size: clamp(1.05rem, 1.35vw, 1.4rem);
  max-width: 60ch;
}

html.present .code {
  font-size: clamp(0.9rem, 1.05vw, 1.15rem);
}

/* Reveal-on-enter is a reading-mode effect. A hidden section never intersects
   the viewport, so its observer would not have fired — leaving the slide blank
   when it appeared. The slide-in animation above replaces it. */
html.present .reveal {
  opacity: 1 !important;
  transform: none !important;
  transition: none !important;
}

html.present .slide-bar {
  display: flex;
  align-items: center;
  gap: 1rem;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 0.6rem clamp(1rem, 4vw, 2.5rem);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  border-top: 1px solid var(--rule);
  z-index: 60;
}

@supports (backdrop-filter: blur(1px)) {
  html.present .slide-bar {
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
  }
}

.slide-count {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--text-muted);
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
}

.slide-progress {
  flex: 1 1 auto;
  height: 3px;
  border-radius: 999px;
  background: var(--surface-strong);
  overflow: hidden;
}

.slide-progress-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 999px;
  transition: width 0.35s var(--ease);
}

/* =========================================================================
   MOTION

   Two mechanisms, chosen deliberately:

   1. Content reveals use IntersectionObserver + a transition, firing once.
      A scroll-linked animation would *reverse* when you scroll back up,
      which looks wrong for text you have already read. Apple's content
      reveals are one-shot too.

   2. Genuinely scroll-linked decoration (the hero settling as you leave it)
      uses `animation-timeline`, behind @supports. It runs off the main
      thread and degrades to simply not happening.

   Everything is scoped to `html.motion`, a class app.rs adds only after the
   WASM module boots. If the module fails to load, no element is ever left
   stranded at opacity 0 — the reader gets a plain, complete page.
   ========================================================================= */

html.motion .reveal {
  opacity: 0;
  transform: translateY(1.75rem);
  transition: opacity 0.75s var(--ease), transform 0.75s var(--ease);
  /* --i is set per child to stagger the cascade rather than popping the
     whole section at once. */
  transition-delay: calc(var(--i, 0) * 65ms);
}

html.motion .reveal.in-view {
  opacity: 1;
  transform: none;
}

/* Scroll-linked hero: fades and settles back as it leaves the viewport.
   Decorative only. */
@supports (animation-timeline: view()) {
  html.motion .hero-linked {
    animation: hero-exit linear both;
    animation-timeline: view();
    animation-range: exit-crossing 0% exit-crossing 100%;
  }
}

@keyframes hero-exit {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0;
    transform: translateY(-2rem) scale(0.97);
  }
}

/* -------------------------------------------------------------------------
   Reduced motion. Every duration collapses and every element stays visible
   — the page must be complete, not merely still.
   ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  html.motion .reveal,
  html.motion .hero-linked,
  html.present section.slide-current {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }
}
