/* ============================================================
   Tanner Bachelor — personal site
   Early-web document structure, current-day fidelity.
   ============================================================ */

:root {
  color-scheme: light;

  --paper:       #fffef8;
  --ink:         #14140f;
  --ink-mid:     #8a8577;
  --rule:        #e4dfce;
  --placeholder: #f7f5eb;   /* stage fill */

  --link:    #0000ee;   /* browser default, on purpose */
  --visited: #551a8b;   /* ditto */
  --active:  #cc2200;
  --green:   #4b5320;   /* brand olive — also the mark's colour */

  /* the sketchpad's marker tray is meant to behave like real markers —
     the pigment doesn't change when you dim the lights. Deliberately
     declared once here and never redefined in the dark block, unlike
     --link/--active/--green above, which DO shift for text contrast. */
  --marker-link:   #0000ee;
  --marker-active: #cc2200;
  --marker-green:  #4b5320;

  --logo-dim: 1;   /* trusted-by logos are drawn light; see dark block */

  --statusbar-h: 2.4rem;   /* bar height AND the body padding that clears it */

  /* One typeface for the whole site. Fallbacks are only a safety net
     for a failed font load — the design targets Geist Mono. */
  --mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* Dark is a token swap and nothing else — no component overrides. The
   inline script in <head> stamps data-theme before first paint, so there
   is never a flash of the wrong theme. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --paper:       #14130d;
  --ink:         #f4f1e6;
  --ink-mid:     #8f8a7a;
  --rule:        #333127;
  --placeholder: #1c1b14;

  /* the default browser blue/purple are unreadable on a dark ground, so
     these are the same hues lifted to carry against it */
  --link:    #8fa8ff;
  --visited: #c4a3e8;
  --active:  #ff6a4d;
  --green:   #bac86a;   /* the flat brand olive is 2.1:1 on this ground —
                            this is the same hue lifted to 9.5:1 */

  /* the logo SVGs have #E4DFCE baked in — near-invisible on light paper,
     but glaring on dark. Dimming keeps their weight the same either way. */
  --logo-dim: 0.5;
}

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  scroll-padding-bottom: 3rem;
}

/* ---------- viewport fill ----------
   The stage stretches to the bottom of the window, so height has to be
   handed down an unbroken chain: body → main → .doc → .col-main → .stage.
   A break anywhere and the stage collapses back to its content height.
   The status bar is fixed (out of flow), so body's bottom padding is what
   reserves its space — and box-sizing:border-box keeps that inside 100vh. */
body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  padding-bottom: calc(var(--statusbar-h) + 1rem);   /* clears the fixed bar */
  background: var(--paper);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 15px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

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

::selection { background: #ffe9a8; color: var(--ink); }

/* Dotted focus ring — a real 90s browser artifact that also happens
   to be the most legible focus indicator there is. */
a:focus-visible,
button:focus-visible {
  outline: 2px dotted var(--ink);
  outline-offset: 3px;
}

a {
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: 3px;
}

a:visited { color: var(--visited); }
a:active  { color: var(--active); }

.wrap {
  max-width: 62rem;
  margin: 0 auto;
  padding: 0 2rem;
}

/* ---------- masthead ---------- */
.masthead { border-bottom: 1px solid var(--rule); }

.masthead-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 1.4rem;
  padding-bottom: 1.4rem;
}

/* 17px, not 24: the old PNG carried ~29% transparent padding, so its ink
   only ever rendered ~17px tall. The SVG is tight-cropped, so it needs the
   ink height directly to keep the masthead weight unchanged. */
/* the artwork's path touches its own viewBox edge with zero margin, and
   inline <svg> (unlike the old <img>) clips to that box by default —
   overflow: visible stops it hairline-clipping the b */
.mark-img { height: 17px; width: auto; display: block; overflow: visible; fill: #4b5320; }

/* brand olive reads fine on paper, but goes muddy on the dark ground —
   swap it for the page's off-white ink instead */
:root[data-theme="dark"] .mark-img { fill: var(--ink); }

.masthead-meta {
  margin: 0;
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: 0.02em;
  color: var(--ink-mid);
  font-variant-numeric: tabular-nums;
}

/* shared by the masthead line and the Elsewhere list */
.sep { margin: 0 0.5em; opacity: 0.6; }

/* ---------- theme toggle ---------- */
.theme-toggle {
  /* sized against the 13px meta line beside it — taller than this and the
     switch starts dominating the masthead */
  --track-w: 36px;
  --track-h: 18px;
  --thumb:   13px;
  --inset:   3px;

  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  color: var(--ink-mid);
}

.toggle-track {
  position: relative;
  display: block;
  width: var(--track-w);
  height: var(--track-h);
  border: 1px solid var(--rule);
  border-radius: 999px;
  transition: border-color 0.25s ease;
}

.theme-toggle:hover { color: var(--ink); }
.theme-toggle:hover .toggle-track { border-color: var(--ink-mid); }

.toggle-thumb {
  position: absolute;
  top: 50%;
  left: var(--inset);
  width: var(--thumb);
  height: var(--thumb);
  margin-top: calc(var(--thumb) / -2);
  /* smooth glide, no overshoot — at this size a bounce has no room to
     clear the track border, so the slide settles clean instead */
  transition: translate 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  translate: 0 0;
}

/* the 2px is the track's own left+right border: `left` is measured from the
   padding box, so the travel has to come off the INNER width or the thumb
   ends up parked hard against the far border */
:root[data-theme="dark"] .toggle-thumb {
  translate: calc(var(--track-w) - 2px - var(--thumb) - var(--inset) * 2) 0;
}

.toggle-icon { display: block; width: 100%; height: 100%; }
.toggle-core { fill: currentColor; }

/* the bite is the page colour, so overlapping it onto the core reads as
   a crescent without needing a mask. Retreating (back to sun) it moves
   immediately; arriving (to moon) it waits for the rays to clear first —
   otherwise the bite sweeps across a still-spiky sun and visibly notches
   a chunk out of it mid-flight, which read as clipping. */
.toggle-bite {
  fill: var(--paper);
  /* parked far enough out to clear the 3 o'clock ray, not just the core */
  translate: 18px -8px;
  transition: translate 0.22s cubic-bezier(0.3, 0, 0.2, 1);
}

:root[data-theme="dark"] .toggle-bite {
  translate: 5px -4px;
  transition: translate 0.26s cubic-bezier(0.3, 0, 0.2, 1) 0.2s;
}

.toggle-rays {
  transform-origin: 12px 12px;
  transition: scale 0.22s ease, opacity 0.2s ease, rotate 0.3s ease;
  /* reappearing (back to sun), wait for the bite to retreat first */
  transition-delay: 0.2s;
}

/* rays shrink and vanish as the moon arrives — immediately, ahead of the
   bite, so nothing ever cuts a notch out of a still-spiky sun */
:root[data-theme="dark"] .toggle-rays {
  scale: 0.3;
  opacity: 0;
  rotate: 45deg;
  transition-delay: 0s;
}

@media (prefers-reduced-motion: reduce) {
  .toggle-thumb,
  .toggle-bite,
  .toggle-rays { transition: none; }
}

/* ---------- document grid ---------- */
/* links in the fill chain: take the leftover height and pass it on */
main { flex: 1; display: flex; flex-direction: column; }

.doc {
  display: grid;
  /* 17rem keeps rail values on one line after the 1.75rem inset */
  grid-template-columns: minmax(0, 1fr) 17rem;
  gap: 2rem;
  padding-top: 2.5rem;
  /* 1.5rem here + the 1rem body already holds past the bar = 2.5rem,
     matching the gap above so the stage sits evenly between the two */
  padding-bottom: 1.5rem;
  flex: 1;
  /* stretch (the default) is what lets the rail's left rule run the
     full height of the row rather than stopping at its own content */
}

.col-main { display: flex; flex-direction: column; }

@media (max-width: 820px) {
  .doc { grid-template-columns: 1fr; gap: 2.5rem; }
  /* stacked: the rail sits below, so a stage that ate the viewport would
     push it off-screen. Back to a fixed ratio here. */
  .stage { flex: none; aspect-ratio: 3 / 2; }
}

/* ---------- hero ---------- */
.hero { margin-bottom: 2.5rem; }

.hero-text {
  margin: 0;
  font-size: 13px;
  line-height: 1.55;
  color: var(--ink);
  max-width: 46rem;
}

.hero-text + .hero-text {
  margin-top: 0.9rem;
}

/* ---------- stage: sketch space ---------- */
/* Height isn't set here — it's whatever is left of the viewport once the
   masthead, hero and status bar have taken theirs. See the fill chain
   under "viewport fill" below. */
.stage {
  position: relative;
  background-color: var(--placeholder);
  /* dot-grid sketch paper, evenly spaced in both directions. Centering the
     tile (rather than the default top-left origin) means there's always a
     dot exactly in the middle of the box, with the rest of the grid
     spreading out symmetrically — so the leftover sliver of space at the
     edges is split evenly between opposite sides instead of piling up on
     the right/bottom, and it re-centers itself automatically whenever the
     box's own width changes. */
  background-image: radial-gradient(circle, var(--rule) 1px, transparent 1.2px);
  background-size: 16px 16px;
  background-position: center;
  border: 1px solid var(--rule);
  width: 100%;
  flex: 1;
  min-height: 12rem;
  overflow: hidden;
  cursor: crosshair;
}

@media (max-width: 820px) {
  .stage { aspect-ratio: 3 / 2; min-height: 12rem; }
}

.stage-canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none; /* JS owns all pointer movement — no scroll/zoom */
}

/* wordmark, printed on the paper — quiet by design, not a decoration
   competing with the sketch. The backing patch is placeholder-coloured
   and slightly larger than the mark itself, so it clears the dot grid out
   from directly behind the letters instead of the dots poking through
   them. Sized numbers here are mirrored in script.js's saveSketch(), so
   a downloaded PNG matches what's on screen. */
.stage-watermark-bg {
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  width: 95px;
  height: 37px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--placeholder);
  pointer-events: none;
}

.stage-watermark {
  width: 75px;
  height: auto;
  fill: var(--rule);
}

/* marker tray: dots sitting in the corner, tucked down at rest, sized
   and aligned to match the save button's circle on the opposite side.
   Hovering (or focusing) one pulls it up like lifting it off the page.
   The selected marker is shown with a small checkmark that draws itself
   on — the same stroke-dashoffset technique design-system checkboxes use
   for their check icon — rather than sitting permanently lifted. */
.stage-tools {
  position: absolute;
  left: 10px;
  bottom: 10px;
  display: flex;
  align-items: flex-end;
  gap: 8px;
}

/* the button itself never moves — it's the hit-test box. Only the dot
   inside it translates. Moving the button itself on :hover was the bug:
   the lift carried its own hit-box out from under a stationary pointer,
   hover dropped, it fell back, hover re-triggered, and it juddered in
   place indefinitely. */
.stage-marker {
  appearance: none;
  padding: 0;
  width: 26px;
  height: 26px;
  border: 0;
  background: none;
  cursor: pointer;
}

.stage-marker-dot {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  border: 1px solid var(--rule);
  translate: 0 0;
  transition: translate 0.32s cubic-bezier(0.33, 1, 0.68, 1);
}

.stage-marker[data-color="ink"]    .stage-marker-dot { background: var(--ink); }
.stage-marker[data-color="link"]   .stage-marker-dot { background: var(--marker-link); }
.stage-marker[data-color="active"] .stage-marker-dot { background: var(--marker-active); }
.stage-marker[data-color="green"]  .stage-marker-dot { background: var(--marker-green); }

.stage-marker:hover .stage-marker-dot,
.stage-marker:focus-visible .stage-marker-dot { translate: 0 -6px; }
.stage-marker:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }

/* checkmark lives inside the dot, so it rides along with the same hover
   lift for free — no separate transform to keep in sync. Drawn as a
   hairline, same 1px weight as every other rule on the page, in the
   paper colour so it reads as a clean cutout against any of the four
   marker colours in either theme, in both of which paper sits at the
   opposite end of the value scale from every marker ink. */
.stage-marker-check {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.stage-marker-check path {
  stroke: var(--paper);
  /* 0.6 in this path's own 16-unit viewBox renders as a true ~1px hairline
     once the SVG is scaled up to the dot's 26px size (26/16 ≈ 1.6x) — same
     approach as the toggle icon's rays elsewhere in this file. Deliberately
     NOT vector-effect: non-scaling-stroke: that remaps stroke-dasharray
     into scaled/device units too, which broke the hide/reveal math below
     into a repeating dashed line instead of a clean draw-in. */
  stroke-width: 0.6;
  /* the path's real length is ~12.03 user units; dasharray/dashoffset are
     set to that (not the SVG pathLength attribute, whose dash-normalizing
     behaviour Safari doesn't reliably honour) so the line is fully hidden
     at rest and fully drawn at 0 in every browser */
  stroke-dasharray: 13;
  stroke-dashoffset: 13;
  transition: stroke-dashoffset 0.32s cubic-bezier(0.33, 1, 0.68, 1);
}
.stage-marker.is-active .stage-marker-check path {
  stroke-dashoffset: 0;
}

@media (prefers-reduced-motion: reduce) {
  .stage-marker-dot { transition: none; }
  .stage-marker-check path { transition: none; }
}

/* quiet by default — only appear once there's actually a sketch to act
   on. Left to right: Clear, Save — an 8px gap apart, same rhythm as the
   marker tray. */
.stage-save,
.stage-clear {
  position: absolute;
  bottom: 10px;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  appearance: none;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--placeholder);
  color: var(--ink-mid);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease, border-color 0.2s ease, color 0.2s ease;
}

.stage-save { right: 10px; }
.stage-clear { right: 44px; }

.stage-save svg,
.stage-clear svg { width: 14px; height: 14px; }
.stage-save:hover,
.stage-save:focus-visible,
.stage-clear:hover,
.stage-clear:focus-visible { border-color: var(--ink-mid); color: var(--ink); }
.stage-save:focus-visible,
.stage-clear:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }

.stage-save.is-visible,
.stage-clear.is-visible { opacity: 1; pointer-events: auto; }

/* lid lifts on hover — the can body stays put. transform-origin sits at
   the lid's left end so it reads as opening rather than just sliding. */
.stage-clear-lid {
  transform-origin: 3.3px 6.2px;
  transition: transform 0.2s cubic-bezier(0.33, 1, 0.68, 1);
}
.stage-clear:hover .stage-clear-lid,
.stage-clear:focus-visible .stage-clear-lid {
  transform: translateY(-1.5px) rotate(-8deg);
}

@media (prefers-reduced-motion: reduce) {
  .stage-clear-lid { transition: none; }
}

/* only the arrow nudges down on hover — the tray itself stays put, same
   as a real download landing in it. Two quick nudges, not one. The arrow
   sits lower than its first draft (moved down to read as optically
   centered), which ate into the headroom above the tray, so the nudge
   amplitude below is trimmed to 1.2px to keep it clear. Plays once on
   hover-in, then settles; re-triggers if the pointer leaves and comes
   back. */
.stage-save:hover .stage-save-arrow,
.stage-save:focus-visible .stage-save-arrow {
  animation: save-nudge 0.5s ease;
}

@keyframes save-nudge {
  0%   { transform: translateY(0); }
  18%  { transform: translateY(1.2px); }
  36%  { transform: translateY(0); }
  54%  { transform: translateY(1.2px); }
  72%  { transform: translateY(0); }
  100% { transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .stage-save,
  .stage-clear { transition: none; }
  .stage-save:hover .stage-save-arrow,
  .stage-save:focus-visible .stage-save-arrow { animation: none; }
}

/* ---------- metadata rail ---------- */
.rail {
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.55;
  border-left: 1px solid var(--rule);
  padding-left: 1.75rem;
}

@media (max-width: 820px) {
  .rail {
    border-left: 0;
    border-top: 1px solid var(--rule);
    padding-left: 0;
    padding-top: 2rem;
  }
}

.rail-list { margin: 0; }

.rail-list dt {
  text-transform: uppercase;
  letter-spacing: 0.09em;
  font-size: 11px;
  font-weight: 400;
  color: var(--ink-mid);
  margin-top: 1.05rem;
  padding-top: 1.05rem;
  border-top: 1px dotted var(--rule);
}

/* first group opens the rail — no rule above it */
.rail-list dt:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.rail-list dd {
  margin: 0.35rem 0 0;
  color: var(--ink);
}

/* keeps a wrapped separator from landing at the start of a line */
.links { line-height: 1.9; }
.links .sep { display: inline-block; }

/* trusted-by logos: all sit in a 50×50 box, so each mark keeps its
   own optical width (IBM wide, Apple narrow) without extra rules */
.logo-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  list-style: none;
  margin: 0.15rem 0 0;
  padding: 0;
}

.logo-row img {
  height: 46px;
  width: 46px;
  opacity: var(--logo-dim);
}

/* ---------- status bar (doubles as the footer) ---------- */
/* Fixed height, set here and reused as the body's bottom padding. The bar
   must NOT resize as the hover text comes and goes — a flex row at a fixed
   height means its contents can change freely without moving anything. */
.statusbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 60;
  height: var(--statusbar-h);
  background: var(--paper);
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1;
  color: var(--ink-mid);
}

/* .wrap supplies the max-width and side padding, so this row ends exactly
   where the masthead's theme toggle ends */
.statusbar-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  height: 100%;
}

/* the href echo — allowed to truncate, never to wrap or push */
.status {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.colophon {
  margin: 0;
  white-space: nowrap;
  flex-shrink: 0;
}

.colophon time { font-variant-numeric: tabular-nums; }

@media (max-width: 640px) {
  /* "Washington, D.C." is flavor text — at this width the full meta line
     (clock / city / toggle) no longer fits on one row, and the toggle
     wraps onto its own orphaned line below. Drop the city and its two
     separators so the clock and toggle stay on a single line. */
  .masthead-meta #clock + .sep,
  .masthead-meta #clock + .sep + span,
  .masthead-meta #clock + .sep + span + .sep {
    display: none;
  }
  .masthead-meta .theme-toggle { margin-left: 0.6em; }

  /* no hover on touch, so the bar stops floating and just ends the page */
  .statusbar {
    position: static;
    height: auto;
    padding: 1rem 0;
  }
  .statusbar-inner {
    height: auto;
    justify-content: flex-start;
  }
  .status { display: none; }
  /* flex-shrink must be released too, or the nowrap width from the desktop
     rule keeps the line rigid and pushes past the viewport */
  .colophon { white-space: normal; flex-shrink: 1; }
  body { padding-bottom: 0; }
}

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