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

html {
  overflow-x: hidden;
}

body {
  font-family: system-ui, sans-serif;
  max-width: 640px;
  width: 100%;
  margin: 4rem auto;
  padding: 0 1rem;
  color: #1a1a1a;
  overflow-x: hidden;
}

code {
  /* Theme-aware (issue #319): the old hardcoded #f0f0f0 chip made dark
     theme's near-white ink unreadable wherever a code span sat in a
     default-ink paragraph (the Streaming intro box was the reported
     sighting). A translucent gray works over both themes' surfaces and
     the text simply inherits the surrounding ink. */
  background: rgba(127, 140, 160, 0.16);
  padding: 0.15rem 0.35rem;
  border-radius: 4px;
}

/* Every canvas in the app is drawn at a fixed intrinsic resolution (see each
   component's WIDTH/HEIGHT constants) but must never force the page wider
   than the viewport on a phone -- this scales it down visually while the
   drag/click handlers correct for the resulting CSS-vs-intrinsic-pixel
   mismatch via `canvasEventPoint` (viewport.ts). `touch-action: none`
   suppresses the browser's own scroll/pinch-zoom gesture during a drag so
   it doesn't fight a component's own pointer-drag handling. */
canvas {
  max-width: 100%;
  height: auto;
  display: block;
  touch-action: none;
}

input,
textarea,
select,
table {
  max-width: 100%;
}

@media (max-width: 640px) {
  body {
    margin: 1.5rem auto;
    padding: 0 0.75rem;
  }
}

/* ==================== SPA shell (mallory's sidebar app pass) ====================
   Scoped entirely under .app-shell so every legacy page above is untouched. */

:root {
  --ground: #fafbfd;
  --surface: #ffffff;
  --ink: #1c2531;
  --ink-soft: #47536b;
  --muted: #64748b;
  --accent: #2563eb;
  --accent-soft: #eaf1ff;
  --border: #e2e8f0;
  --border-soft: #edf1f6;
  --danger: #dc2626;
}
@media (prefers-color-scheme: dark) {
  :root {
    --ground: #0b1220;
    --surface: #111a2b;
    --ink: #e7ecf5;
    --ink-soft: #aab6cc;
    --muted: #7c8aa5;
    --accent: #5b8def;
    --accent-soft: #16233d;
    --border: #223049;
    --border-soft: #1a2537;
    --danger: #f87171;
  }
}
:root[data-theme="dark"] {
  --ground: #0b1220;
  --surface: #111a2b;
  --ink: #e7ecf5;
  --ink-soft: #aab6cc;
  --muted: #7c8aa5;
  --accent: #5b8def;
  --accent-soft: #16233d;
  --border: #223049;
  --border-soft: #1a2537;
  --danger: #f87171;
}
:root[data-theme="light"] {
  --ground: #fafbfd;
  --surface: #ffffff;
  --ink: #1c2531;
  --ink-soft: #47536b;
  --muted: #64748b;
  --accent: #2563eb;
  --accent-soft: #eaf1ff;
  --border: #e2e8f0;
  --border-soft: #edf1f6;
  --danger: #dc2626;
}

/* The shell needs the full viewport width; the plain-page rule above (max-width: 640px,
   centered) is exactly what every legacy page still wants, so it's only lifted here. */
body:has(.app-shell) {
  max-width: none;
  margin: 0;
  padding: 0;
  background: var(--ground);
  color: var(--ink);
}

.app-shell {
  display: grid;
  grid-template-columns: 220px 1fr;
  min-height: 100vh;
}
.mono {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}
.sidebar {
  background: var(--surface);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 1.2rem;
  padding: 1rem 0.85rem;
}
.wordmark {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  padding: 0.1rem 0.4rem 0.55rem;
  border-bottom: 1px solid var(--border-soft);
}
.wordmark-accent {
  color: var(--accent);
}
.wordmark-link {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  color: inherit;
  text-decoration: none;
}
.wordmark-link:hover {
  opacity: 0.85;
}
.theme-toggle {
  margin-left: auto;
  border: 1px solid var(--border);
  background: var(--ground);
  color: var(--muted);
  border-radius: 6px;
  width: 26px;
  height: 26px;
  cursor: pointer;
}
.theme-toggle-icon {
  transition: transform 0.15s ease;
}
.primary-nav {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.nav-eyebrow {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.66rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 0 0.5rem 0.3rem;
}
.nav-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.6rem;
  border-radius: 8px;
  color: var(--ink-soft);
  text-decoration: none;
  font-size: 0.88rem;
}
.nav-item:hover {
  background: var(--border-soft);
  color: var(--ink);
}
.nav-item.active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}
.sidebar-footer {
  margin-top: auto;
  border-top: 1px solid var(--border-soft);
  padding-top: 0.7rem;
}
.legacy-link {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.6rem;
  color: var(--muted);
  text-decoration: none;
  font-size: 0.78rem;
  border-radius: 8px;
}
.legacy-link:hover {
  background: var(--border-soft);
}
.app-main {
  padding: 1.6rem 2.2rem 3rem;
  max-width: 980px;
  /* Grid items default to min-width:auto, meaning they refuse to shrink
     below their content's own preferred width -- a page with a hard,
     non-shrinking minimum inside it (e.g. Linked3DView's fixed 600px 3D
     canvas container) then forces .app-main past its actual grid-column
     width, blowing the whole page out past the viewport. Since html/body
     both set overflow-x:hidden (so no *legitimate* page ever grows an
     accidental horizontal scrollbar), that overflow was silently clipped
     and unreachable rather than scrollable -- reported as "impossible to
     scroll right" on 3D & Surfaces specifically, since it's the one page
     whose content has a wide-enough non-shrinking minimum to trip this.
     min-width:0 lets .app-main shrink to its actual grid track/viewport
     width instead, so the max-width:100% canvas rule and every other
     shrink-friendly child can do their job. */
  min-width: 0;
}
.page-eyebrow {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 0.35rem;
}
.page-head h1 {
  font-size: 1.6rem;
  margin: 0 0 0.4rem;
  letter-spacing: -0.015em;
  text-wrap: balance;
}
.lede {
  color: var(--ink-soft);
  max-width: 60ch;
  line-height: 1.55;
  margin: 0 0 1.5rem;
}
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 0.85rem;
}
.dashboard-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1rem 1.05rem;
  text-decoration: none;
  color: var(--ink);
  display: block;
}
.dashboard-card:hover {
  border-color: var(--accent);
}
.dashboard-card h3 {
  margin: 0 0 0.4rem;
  font-size: 0.98rem;
}
.dashboard-card p {
  margin: 0;
  color: var(--muted);
  font-size: 0.83rem;
  line-height: 1.45;
}
.demos-strip {
  margin-top: 2rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--border-soft);
  font-size: 0.82rem;
  color: var(--muted);
}
.tab-row {
  display: flex;
  gap: 0.3rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: 1.1rem;
  /* #378: on a narrow (mobile) viewport this row is often wider than the
     screen -- without this, flex's default nowrap kept every button on one
     line but let the ROW itself overflow the page (nothing to scroll it
     into view), while each button's own label text wrapped onto 2 lines
     instead (no `white-space: nowrap` below), producing the "neither fits
     nor scrolls" layout the issue's screenshot shows. `overflow-x: auto`
     turns the row itself into the thing that scrolls horizontally; -webkit-
     overflow-scrolling gives that scroll the native momentum/inertia feel
     on iOS Safari (elsewhere, this property does nothing at all -- it's not
     part of the CSS overflow shorthand family, so no `overflow-y` couples
     to it, it's a plain no-op on browsers that never implemented it). */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.tab-button {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.78rem;
  padding: 0.5rem 0.85rem;
  color: var(--muted);
  border: none;
  border-bottom: 2px solid transparent;
  background: none;
  cursor: pointer;
  /* Keeps each label on one line (see .tab-row's own comment) and stops a
     button from being squeezed by its flex siblings once the row is
     scrollable instead of wrapping -- every button keeps its natural
     content width regardless of how many siblings it has. */
  white-space: nowrap;
  flex-shrink: 0;
}
.tab-button.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
  font-weight: 600;
}
/* Fixed (not theme-token) colors: legacy pages aren't dark-mode-aware (see
   the plain `body` rule above), so this shouldn't flip with the OS/app
   theme either -- it would mismatch the rest of the still-light page. */
.legacy-banner {
  background: #f5f7fa;
  border: 1px solid #e2e8f0;
  border-radius: 6px;
  padding: 0.5rem 0.75rem;
  font-size: 0.85rem;
  color: #47536b;
}

@media (max-width: 720px) {
  .app-shell {
    grid-template-columns: 1fr;
    /* At this width .sidebar and .app-main split into two separate grid
       rows (desktop keeps them side by side in one row, where stretching
       both to fill .app-shell's min-height:100vh is exactly the desired
       "sidebar spans the full page" look). Rows default to auto-sized, and
       grid's align-content:normal computes as stretch -- so any leftover
       space between the two rows' natural content height and 100vh was
       being split *evenly* across both auto rows, inflating .sidebar's row
       by the same absolute amount as .app-main's. Since .sidebar's own
       content is short (~85px), that stretch bonus nearly doubled its
       height on any page whose .app-main content was itself short (Calculator,
       Geometry, Gallery) -- reported as the header looking "extra tall" on
       exactly those pages and normal everywhere else, where .app-main's own
       content already exceeds 100vh and leaves nothing to distribute.
       grid-template-rows: auto 1fr pins .sidebar's row to its actual content
       size and hands 100% of any leftover space to .app-main's row instead. */
    grid-template-rows: auto 1fr;
  }
  .sidebar {
    flex-direction: row;
    align-items: center;
    border-right: none;
    border-bottom: 1px solid var(--border);
    padding: 0.6rem 0.7rem;
    gap: 0.75rem;
    overflow-x: auto;
    position: sticky;
    top: 0;
    z-index: 5;
  }
  .wordmark {
    border-bottom: none;
    padding: 0 0.3rem 0 0;
    flex: none;
  }
  .primary-nav {
    flex-direction: row;
    gap: 0.25rem;
    flex: none;
  }
  .nav-eyebrow {
    display: none;
  }
  .nav-item {
    flex-direction: column;
    gap: 0.2rem;
    font-size: 0.62rem;
    padding: 0.35rem 0.5rem;
    white-space: nowrap;
  }
  .sidebar-footer {
    margin-top: 0;
    border-top: none;
    padding-top: 0;
    flex: none;
  }
  .app-main {
    padding: 1.2rem 1.1rem 2.5rem;
  }
}

/* Issue #53's remaining "slider/transport controls sized for touch on the
   keyframe timeline" item: `(pointer: coarse)` (true for touchscreens,
   false for a mouse/trackpad even on a touch-capable laptop) rather than
   `(max-width: ...)`, since a touch-vs-mouse distinction is about input
   precision, not viewport size -- a touch tablet in landscape at a wide
   viewport still needs the larger targets a narrow-viewport media query
   wouldn't catch. Scoped to `.timeline-slider`/`.timeline-control`
   (TransportControls.tsx, KeyframeSliderControl.tsx) rather than every
   `input[type="range"]` app-wide -- other sliders (e.g. area-shading
   bounds) are plain small numeric steppers, not something a thumb drags
   along a track, so widening them isn't the same ask. */
@media (pointer: coarse) {
  .timeline-slider {
    height: 2rem;
  }
  .timeline-slider::-webkit-slider-thumb {
    width: 28px;
    height: 28px;
  }
  .timeline-slider::-moz-range-thumb {
    width: 28px;
    height: 28px;
    border: none;
  }
  .timeline-control button,
  .timeline-control input[type="checkbox"] {
    min-width: 44px;
    min-height: 44px;
  }
}

/* Issue #50's focus-visible audit: this app had NO focus styling anywhere
   -- every interactive element (buttons, inputs, links, and now the
   keyboard-navigable graph canvases) relied entirely on the browser's own
   default outline, which several browsers suppress or barely render on
   dark backgrounds. `:focus-visible` (not plain `:focus`) so a mouse click
   still doesn't show a ring, only actual keyboard/assistive-tech focus. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Floating cross-page calculator (issue #340): a fixed-position dock,
   collapsed to a small toggle by default so it never covers canvas-heavy
   pages (Graphing, 3D, Geometry, ...) uninvited -- the user opts in per
   session by clicking it, not something that persists across reloads (see
   _app.tsx's own doc comment). z-index above .app-main's content but below
   nothing else fixed-position currently exists in this app. */
.floating-calculator-toggle {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 20;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface);
  font-size: 1.25rem;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  cursor: pointer;
}
.floating-calculator-panel {
  position: fixed;
  right: 1rem;
  bottom: 4.25rem;
  z-index: 20;
  width: min(22rem, calc(100vw - 2rem));
  max-height: min(32rem, calc(100vh - 6rem));
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
  padding: 0.6rem;
}
.floating-calculator-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.4rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--border-soft);
  font-weight: 600;
}
