@font-face {
  font-family: "dugfont";
  src: url("assets/fonts/dugfont.ttf") format("truetype");
  font-weight: bold;
}
* { box-sizing: border-box; }
html, body {
  margin: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #1a1a1a;
  font-family: -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
}
#stage {
  position: relative;
  width: 100%;
  height: 100%;
  overflow-x: hidden;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
}
#content {
  position: relative;
  width: 100%;
  min-height: 100%;
}
#content::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-repeat: repeat;
  background-image: var(--clay-bg-image);
  background-size: var(--clay-bg-size);
  filter: var(--clay-bg-filter);
}
.cmd-rect { position: absolute; }
/* Images stay a pure decorative overlay, never independently interactive
   (there's no click behavior tied to an icon, only conceptually to the
   button surface beneath it) -- pointer-events: none lets the mouse "see
   through" them to whatever rect is underneath for any DOM-hit-test-based
   interaction (elementFromPoint, real event listeners added later, ...).
   Not what button.yp's OWN hover/press feedback depends on, though -- see
   renderStage's pointerX/pointerY comment in renderer.js for why that's
   computed from real cursor coordinates against draw-command bounding
   boxes instead of CSS :hover/DOM hit-testing at all.
   Text is deliberately NOT included here (it used to be) -- selectable
   text needs real pointer-events so the browser gets the mousedown/drag
   gesture that starts a selection. That's safe to allow now (it wasn't
   before, but for an unrelated reason): with renderStage keeping a
   persistent element per Clay id instead of tearing down and rebuilding
   #content every animation frame, a Selection Range on a .cmd-text node
   actually survives from one frame to the next instead of being silently
   invalidated ~16ms later when its backing node got destroyed. */
.cmd-img { pointer-events: none; }
.cmd-img { position: absolute; }

/* button.yp clay-button surfaces: tagged by renderer.js (see its
   comment on the DRAW_RECT branch of renderStage) whenever a rect draw
   command's is_clay_surface flag is set. Its OWN texture + hue
   (--clay-btn-bg-*, picked once in pickButtonTexture) is layered on via
   ::before rather than the `filter` property directly on this element,
   so the grayscale/sepia/hue-rotate normalization filter never touches
   this element's own box-shadow -- exactly the same split #content /
   #content::before already uses for the page background, just scoped to
   one button instead of the whole page. */
.cmd-rect.clay-surface {
  cursor: pointer;
  /* #clay-edge-wobble (defined as an inline SVG filter just after
     <body>) both bends this element's whole silhouette into an
     irregular, hand-carved-looking boundary AND relights its surface
     to actually look convex at every edge -- see that filter's own
     comment for why box-shadow alone (an EARLIER version of this rule)
     could never do the second part: box-shadow only ever paints
     gradients INSIDE a still perfectly flat, hard-edged box, it can't
     change how the edge itself reads, so no amount of blur tuning made
     it look genuinely rounded rather than "a flat rectangle with
     shading near its border." Applied to the OUTER element (not
     ::before) on purpose: text/icon are separate sibling divs, not
     descendants, so they never pass through this filter and stay
     crisp. */
  filter: url(#clay-edge-wobble);
  /* Only the GAP to the surrounding wall is left as box-shadow now --
     a real cast shadow from a raised object onto what's behind it,
     which genuinely is a separate flat layer sitting behind/around the
     button, unlike the button's own surface curvature (now handled
     entirely by the filter above). Barely visible on the lit
     (top/left) side, a hard, dark, almost-unblurred shadow on the far
     (bottom/right) side -- matches the 9 real cutouts in
     assets/textures/clay/buttons/, cropped straight out of the
     reference screenshot.
     Those same cutouts also show a second, DIFFERENT dark line: a
     thin, crisp score running the WHOLE way around the button --
     visible even on the lit top/left side, where the broad directional
     cast shadow above never reaches at all -- but NOT a fixed-width
     ring hugging the button's own edge (a plain "0 0 0 2px" box-shadow
     ring, tried first, always looks like an outline precisely tracing
     the shape, since box-shadow always is exactly that; the real thing
     visibly bulges outward in places and pinches back in almost to the
     button in others). That's the #clay-edge-wobble filter's job now
     (see its own grooveRing comment) -- an independently-shaped, dilated
     + separately-noise-displaced silhouette merged in behind this
     element, so it stops being tied to this box's own geometry at all. */
  box-shadow:
    3px 4px 2px -1px rgba(10, 6, 3, 0.65),
    4px 6px 6px rgba(0, 0, 0, 0.22),
    -1px -1px 2px rgba(255, 226, 200, 0.3);
  transition: box-shadow 0.12s ease;
}
/* Sits at the exact same position/size/radius as its companion
   .clay-surface (see renderStage's "groove" element in renderer.js),
   but is a wholly separate element -- never gets is-hover/is-active or
   any transform, so the wall groove it draws (via #clay-groove, an
   independently-shaped silhouette, NOT this box's own edge) stays put
   when the button on top of it presses down. pointer-events: none so
   it can never itself become the hover/click target. */
.cmd-rect.clay-groove {
  pointer-events: none;
  filter: url(#clay-groove);
}
/* Plain flat backing behind the button, same box, no filter -- always
   fully hidden while the button rests on top of it. #clay-groove above
   is deliberately transparent under the button's own footprint (it only
   draws the ring OUTSIDE it), so without this, sliding the button down
   2px on :active would uncover a sliver of the page's own wall texture
   at its top edge instead of the groove's dark color -- see renderStage
   in renderer.js for the fuller version of this comment. */
.cmd-rect.clay-socket {
  pointer-events: none;
  background: #231208;
}
/* divider.yp's standalone rule -- #clay-groove-line (index.html) is the same
   carved-material recipe as .clay-surface's own companion #clay-groove
   filter, just producing a single solid wobbly-edged line instead of a ring
   (see that filter's comment for the construction, and divider.yp's for why
   there's no button here to carve a ring around). No cursor/hover/press
   styling and no groove/socket pair of its OWN -- a divider isn't clickable
   and IS the groove, not a raised surface that needs one carved around it. */
.cmd-rect.clay-groove-line {
  pointer-events: none;
  filter: url(#clay-groove-line);
}
.cmd-rect.clay-surface::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background-repeat: repeat;
  background-image: var(--clay-btn-bg-image);
  background-size: var(--clay-btn-bg-size);
  background-position: var(--btn-bg-pos, 0 0);
  filter: var(--clay-btn-bg-filter);
  transition: filter 0.12s ease;
}
/* Real hover/press feedback -- .is-hover/.is-active, NOT :hover/:active.
   These divs get rebuilt from scratch every animation frame (see
   renderer.js's renderStage), so a .clay-surface element never persists
   long enough for the browser's own :hover/:active bookkeeping to be
   reliable on it (confirmed directly: a synthetic-pointer test showed
   the element's own :hover matching every frame, yet the ::before
   filter it should have driven never actually painted -- real
   before/during-hover screenshots came out pixel-identical). renderStage
   computes hover/press itself from real cursor coordinates against that
   frame's OWN bounding box instead, and adds a plain class while
   building the (already fresh) div, so it's simply born in the right
   state -- see its pointerX/pointerY comment in renderer.js. No click
   ACTION is wired to either state (see button.yp's header comment). */
.cmd-rect.clay-surface.is-hover::before { filter: var(--clay-btn-bg-filter) brightness(1.12); }
.cmd-rect.clay-surface.is-active {
  /* Pressed in: the offset shadow/highlight pair above exists because
     the button sits PROUD of the wall -- push it down and that gap
     mostly closes, so both shrink toward the button's own edge instead
     of vanishing outright, and the surface itself darkens all over
     (no longer catching much light at all while sunk in). The actual
     2px downward shift itself is applied in renderer.js as a real
     `top` change, not a transform here -- see that file's comment on
     why. */
  box-shadow:
    1px 2px 1px rgba(10, 6, 3, 0.75),
    -1px -1px 2px rgba(255, 226, 200, 0.15),
    inset 5px 5px 9px rgba(0, 0, 0, 0.4),
    inset -3px -4px 7px rgba(0, 0, 0, 0.25);
}
.cmd-rect.clay-surface.is-active::before { filter: var(--clay-btn-bg-filter) brightness(0.86); }

/* button.yp panel() surfaces: a raised, textured content panel -- its own
   curated texture pool (--clay-panel-bg-*, assets/textures/clay/pane/,
   picked+normalized once in pickPanelTexture), sharing only the page's
   random hue with .clay-surface above, not the literal same image. No
   cursor/hover/press styling: unlike a button, a panel has no click
   action, so it shouldn't look clickable --
   also why there's no groove/socket pair like .clay-surface's, since those
   only exist to sell a button sinking into the wall on press.
   Deliberately no LIVE per-pixel filter here (#clay-edge-wobble's own
   recipe, or two earlier attempts at a cheaper variant of it) -- see
   renderer.js's header comment above panelClipPath for the full history;
   short version, a filtered element's rendered output is one bitmap the
   size of its whole box that the compositor can't tile/cache
   incrementally, so a panel potentially thousands of px tall kept forcing
   a huge bitmap rebuild (visible as the panel flashing solid black) on
   every scroll-triggered layer eviction, no matter how the filter's own
   cost was trimmed down. `clip-path` here is pure geometry (renderer.js
   computes+caches the wobbly polygon once per panel, applyPanelClip) --
   cheap for the compositor regardless of element size, no raster region
   involved at all. `--panel-clip` is set on this element by renderer.js
   and inherited into ::before/::after below (custom properties inherit
   into pseudo-elements by default) so the texture and bevel layers share
   the exact same wobbly outline instead of each guessing their own. */
.cmd-rect.clay-panel {
  clip-path: var(--panel-clip);
  /* Stands in for feDiffuseLighting's per-pixel bevel lighting -- two
     drop-shadows (NOT box-shadow: box-shadow would just get clipped away
     anywhere it falls outside clip-path's own path, since clip-path
     restricts this whole element's paint, shadow included -- drop-shadow
     is computed from the actual rendered, already-clipped alpha shape, so
     it correctly hugs the wobbly outline instead of a straight-edged
     rectangle). Same two-tone read as .clay-surface's own box-shadow: a
     barely-there highlight on the lit (top/left) side, a soft, broader
     dark cast shadow toward the far (bottom/right) side. */
  filter: drop-shadow(-1px -1px 1px rgba(255, 226, 200, 0.25)) drop-shadow(4px 6px 10px rgba(0, 0, 0, 0.35));
}
.cmd-rect.clay-panel::before {
  content: "";
  position: absolute;
  inset: 0;
  clip-path: var(--panel-clip);
  pointer-events: none;
  background-repeat: repeat;
  /* Own texture pool (assets/textures/clay/pane/, picked+normalized in
     pickPanelTexture) -- NOT --clay-btn-bg-* (buttons' own dedicated
     asset) -- so a panel reads as a distinct slab of material, sharing
     only the page's random hue, not literally the same texture image as
     every button. */
  background-image: var(--clay-panel-bg-image);
  background-size: var(--clay-panel-bg-size);
  background-position: var(--panel-bg-pos, 0 0);
  /* --panel-hue-filter is only ever set (renderer.js's applyPanelHue) on a
     panel whose caller passed panel(.hue = ...) -- var()'s fallback means
     every OTHER panel, with that property left unset, just falls straight
     through to the same shared --clay-panel-bg-filter every panel used to
     use unconditionally. */
  filter: var(--panel-hue-filter, var(--clay-panel-bg-filter));
}
/* The actual "this is a domed 3D surface, not a flat rectangle" read --
   pulled OUT of .clay-panel itself (an earlier version put the inset
   box-shadow there directly) because ::before above paints ON TOP of its
   own originating element's background/box-shadow -- a pseudo-element is
   stacked like a child, so ::before's own opaque texture image was
   completely hiding an inset shadow painted on the parent underneath it
   (confirmed directly: identical box-shadow values, visible in an
   isolated test with no covering layer, invisible once the real
   ::before's texture sat on top). A separate ::after, painted AFTER (so
   on top of) ::before, has no background of its own -- nothing but the
   shadow -- so it composites visibly over the texture instead of being
   buried under it. INSET shadows specifically (unlike the outer
   drop-shadow above) never paint past this box's own border edge in the
   first place, so clip-path cutting it down to its wobbly silhouette
   doesn't cut them off oddly the way a plain OUTER box-shadow would (see
   .clay-panel's own filter comment). Two soft, wide, oppositely-lit
   layers -- the same highlight-top-left/shadow-bottom-right convention
   #clay-edge-wobble's own feDistantLight uses, just painted as a fixed
   falloff from each edge inward instead of computed per-pixel -- read as
   a broad, gentle dome across the whole surface. Exactly the same
   ingredient (inset box-shadow) button.yp's own .is-active state already
   uses for its pressed-IN look (below) -- same technique, opposite
   direction: light-in from the lit corner instead of dark-in from every
   corner. */
.cmd-rect.clay-panel::after {
  content: "";
  position: absolute;
  inset: 0;
  clip-path: var(--panel-clip);
  pointer-events: none;
  /* Two layers per side, same "broad soft dome + a tighter crisp rim"
     combination .clay-surface.is-active's own inset pair already uses for
     its pressed-in look (below) -- one wide/faint layer reading as the
     overall curve, one narrower/stronger layer giving the edge itself a
     more defined lip instead of everything being one same-rate falloff.
     The broad layer's own offset (55-70px) looks disproportionately large
     next to a button's equivalent (a button's own box-shadow numbers are
     all single-digit-to-teens px) -- confirmed directly, box-shadow's
     blur falloff does NOT scale 1:1 with element size the way it might
     seem like it should: the exact same offset/blur numbers that read as
     a clear, broad dome on a small test box came out visibly flatter on a
     box a couple thousand px tall, and flatter still on the real
     (~760x2500+) panel, so the panel needs an offset several times larger
     than a small card would to reach comparably far in. Blur kept
     tighter relative to that offset (compare button()'s own is-active
     blur, always a modest fraction of its offset) than an earlier version
     of this rule did, and both layers' own opacity pushed up, so the
     light-to-dark falloff happens over comparatively less distance --
     reads as a steeper curve, not just a wider hazy one. Highlight color
     is the same warm cream .clay-surface's OWN resting box-shadow
     highlight uses (rgba(255, 226, 200, ...), see that rule below) rather
     than flat white -- confirmed directly, an earlier version of this
     rule still had the highlight reaching broadly across a big chunk of
     the surface (same order of magnitude offset/blur as the dark side,
     just lower opacity), which read as a wide desaturated gray-ish wash
     where the warm highlight and dark shadow overlapped, not a crisp lit
     edge. Cut down here to a small, tight, low-opacity corner accent --
     "just a little" lit, not a broad wash -- while the dark side stays
     the dominant, far-reaching cue for the curve (a raised clay surface
     catching ambient light shouldn't read as brighter than the dark side
     reads dark to begin with). */
  box-shadow:
    inset 26px 30px 40px rgba(255, 226, 200, 0.16),
    inset -60px -70px 110px rgba(0, 0, 0, 0.58),
    inset 6px 6px 7px rgba(255, 226, 200, 0.14),
    inset -8px -10px 12px rgba(0, 0, 0, 0.42);
}
/* Same "solid core, then a smooth blurred falloff to nothing" shape as
   #clay-groove's own construction (see index.html's inline SVG) -- one
   plain 2-layer text-shadow always reads as a hard inner ring plus a
   separate, visibly DIFFERENT soft outer ring (two discrete steps, not a
   gradient), the same "flat shape with shading near the edge" problem
   #clay-edge-wobble's comment describes box-shadow alone running into on
   the button surface. Stacking MORE steps -- tight/strong down to
   wide/faint -- approximates a continuous gradient the way a card of
   increasingly-blurred, increasingly-transparent copies always does in
   CSS (no real feGaussianBlur equivalent exists for text-shadow itself),
   so the ink appears to build up smoothly out of the halo into its own
   full solid color instead of snapping straight from "soft glow" to
   "flat fill" at one crisp boundary. */
.cmd-text {
  position: absolute;
  /* Each .cmd-text div is already exactly ONE of Clay's own final wrapped
     lines (Clay emits one TEXT render command per wrapped line, see
     app.yp's write_rect_command header comment) -- never pre-wrap here:
     that lets the browser silently re-wrap a line a SECOND time onto an
     internal second visual row whenever its real rendered width comes out
     even slightly wider than Clay's own per-line width estimate (c.w,
     set as this div's CSS width in renderer.js) -- which, now that
     measure_text's width multiplier is calibrated close to the font's
     real average (see that fn's header comment), happens for any
     individual line whose real content skews a bit denser than average.
     Confirmed directly: switching this to pre-wrap reproduced visible
     garbled-looking overlap between a line's own self-wrapped remainder
     and the next sibling positioned right below it; plain `pre` never
     wraps at all, so any per-line estimate slop just overflows the box
     horizontally instead (harmless -- overflow stays visible below,
     unclipped, same as it already was for vertical overflow). */
  white-space: pre;
  font-family: "dugfont", -apple-system, sans-serif;
  line-height: 1.5;
  text-shadow:
    0 1px 1px rgba(0, 0, 0, 0.75),
    0 1px 3px rgba(0, 0, 0, 0.55),
    0 2px 6px rgba(0, 0, 0, 0.35),
    0 3px 10px rgba(0, 0, 0, 0.18);
  overflow: visible;
}
body.light-bg .cmd-text {
  text-shadow:
    0 1px 1px rgba(255, 255, 255, 0.75),
    0 1px 3px rgba(255, 255, 255, 0.55),
    0 2px 6px rgba(255, 255, 255, 0.35),
    0 3px 10px rgba(255, 255, 255, 0.18);
}
