/* ============================================================================
   VHS camcorder playback — Based In (basedin3 clone), /glaze skill
   Recipe: ~/.claude/skills/glaze/references/styles/vhs-camcorder.md

   Played-back consumer-tape look through a camcorder/deck — NOT a CRT
   (see the sibling crt.css this replaces on basedin3; that recipe simulates
   the TUBE, this one simulates the TAPE/SIGNAL). No blinking REC light, no
   record indicator — signature element is the running timecode counter.

   Grounded in 12 frames pulled from a real analog reference clip spanning
   its full 56.7s (incl. a tracking-glitch frame at 0:01): running timecode
   HH:MM:SS:FF white-on-black bottom-center; soft blur everywhere; washed-out
   highlight-biased pale green/cream grade (warm reds survive it); red/cyan
   channel-split on vertical edges; ragged (not clean) black side borders;
   a permanent scrambled noise band at the very bottom edge (head-switching);
   fine grain; and brief randomized damage bursts (white dropout streaks +
   wavy band tearing) that degrade then settle back to the stable look.

   Architecture mirrors crt.css's: one fixed, pointer-events:none overlay
   stack on #vellum-main (the shared Vellum wrapper every template renders
   inside — site-wide via a single edit), CSS custom-property signal state,
   canvas layers for noise, ONE interactive sibling (.vhs-controls). But the
   technique set is different end to end: no curvature, no scanlines, no
   phosphor mask, no bezel/shell — this is tape damage, not tube geometry.

   All intensities are CSS custom properties on :root (recipe's parameter
   list). --vhs-wear is the runtime MASTER multiplier the on-screen slider
   drives (0 = Off's clean-page state, 1 = full base-look intensity); the
   base --vhs-* values below are the per-element design-time defaults the
   recipe tuned against the reference frames.
   ========================================================================= */

:root {
	--vhs-blur: 0.55px;         /* base gaussian blur, desktop */
	--vhs-sat: 0.72;            /* base saturation multiplier (1 = neutral) */
	--vhs-contrast: 0.97;       /* base contrast multiplier */
	--vhs-bright: 1.03;         /* base brightness multiplier (faded-print lift) */
	--vhs-ca: 0.8;              /* base chromatic-aberration offset, px */
	--vhs-grade: 0.16;          /* highlight-tint overlay opacity */
	--vhs-grain: 0.10;          /* grain canvas opacity */
	--vhs-headswitch: 0.85;     /* head-switch noise band opacity */
	--vhs-border: 2vw;          /* side-border strip width, desktop */
	--vhs-border-blur: 2.5px;   /* softens the undulating edge */
	--vhs-border-ca: 2.2px;     /* red/cyan fringe offset tracing the wavy edge */
	--vhs-burst-min: 6;         /* seconds, min gap between damage bursts */
	--vhs-burst-max: 20;        /* seconds, max gap between damage bursts */
	--vhs-burst-amt: 1;         /* 0-1 master intensity of a burst */
	--vhs-timecode-opacity: 1;
	--vhs-headswitch-h: 2.1vh;  /* head-switch band height */
	--vhs-wear: 1;              /* RUNTIME master 0-1 — the "Wear" slider + Off toggle drive this */
	--vhs-box-h: 52px;          /* desktop only: shared height for the timecode readout and the
	                                Tape controls box, so they match exactly regardless of either
	                                one's own content — see both rules under (min-width: 768px) below. */
}

@media (max-width: 640px) {
	:root { --vhs-border: 3.2vw; --vhs-headswitch-h: 1.9vh; }
}

/* ── main signal chain: blur + chromatic aberration + tear (burst) +
   grade, all in ONE CSS filter list on #vellum-main so they compose as a
   single GPU filter pass. SVG filters (#vhs-ca, #vhs-tear) and CSS filter
   functions chain freely in one `filter:` value. Every term lerps toward
   its NEUTRAL value as --vhs-wear -> 0, so the Off toggle (which zeroes
   --vhs-wear) reaches a genuinely clean, unfiltered page — not just a
   hidden overlay sitting on top of a still-filtered one. ────────────────── */
#vellum-main {
	filter:
		blur(calc(var(--vhs-blur) * var(--vhs-wear)))
		url(#vhs-ca)
		url(#vhs-tear)
		saturate(calc(1 + (var(--vhs-sat) - 1) * var(--vhs-wear)))
		contrast(calc(1 + (var(--vhs-contrast) - 1) * var(--vhs-wear)))
		brightness(calc(1 + (var(--vhs-bright) - 1) * var(--vhs-wear)));
}

@media (prefers-reduced-motion: reduce) {
	/* the STEADY-STATE filtered look stays (it's a texture, not motion) —
	   only the things that move under it stop; see the .vhs-off /
	   prefers-reduced-motion rules further down for grain/headswitch/border. */
	#vellum-main { filter:
		blur(calc(var(--vhs-blur) * var(--vhs-wear)))
		url(#vhs-ca)
		saturate(calc(1 + (var(--vhs-sat) - 1) * var(--vhs-wear)))
		contrast(calc(1 + (var(--vhs-contrast) - 1) * var(--vhs-wear)))
		brightness(calc(1 + (var(--vhs-bright) - 1) * var(--vhs-wear)));
	} /* #vhs-tear dropped entirely — no bursts run under reduced motion */
}

/* ── the glass stack: fixed, full-viewport, pointer-events:none end to
   end. The ONE interactive element (.vhs-controls) is printed as its own
   sibling below, never a child — same rule as crt.css, same reason (the
   glass never needs a pointer-events hole punched in itself). ──────────── */
.vhs-glass {
	position: fixed;
	inset: 0;
	z-index: 400;
	pointer-events: none;
}
/* RE-ENABLED on mobile (was display:none — see backup files for the old
   reasoning). .vhs-glass is position:fixed + inset:0 (viewport-bounded,
   never document-height) and its own filtered children (.vhs-border-fx)
   are similarly bounded — none of them hit the document-height
   rasterization ceiling #vellum-main's filter did (see that rule's own
   history in backup files). vhs.js already throttles this stack hard on
   mobile independent of this CSS (isMobile: lower canvas DPR, ~1800 vs
   4000 grain particles, ~7fps vs ~12fps grain/headswitch redraw, half the
   border wave points) — so re-enabling it here isn't turning on an
   unthrottled effect, just un-hiding an already mobile-tuned one. */

/* EMERGENCY RE-REVERT (2026-08-13, same day): removing the mobile
   override above DID reproduce the "content not loading" crash on real
   hardware within minutes — confirms the mechanism, not a coincidence.
   filter:none goes back here so the site loads again. Jordan still wants
   the SAME filter intensity on mobile as desktop, not a toned-down
   version — the real fix for that (keeping this exact filter, still no
   crash) is restructuring #vellum-main into a fixed, viewport-sized,
   internally-scrolling container, so the filter only ever has to
   rasterize ~100vh of content instead of the full document height,
   regardless of how long the page actually is. That's a real, bigger
   change (affects site-wide scroll behavior — nav scroll-detection,
   anchor links, the sticky CTA, any scroll-position JS) worth building
   deliberately rather than shipping blind onto a site that was just
   confirmed broken. This filter:none is the safe holding state while
   that gets built, not the final answer. */
@media (pointer: coarse) {
	#vellum-main { filter: none; }
}

/* ── grain — fine, constant texture (recipe point 7). Redrawn on an
   interval by vhs.js (12-15fps — indistinguishable from full-rate grain,
   a third the cost). Opacity lerps to 0 with --vhs-wear so Off truly
   removes it, not just visually. ────────────────────────────────────────── */
#vhs-grain {
	position: absolute; inset: 0; width: 100%; height: 100%;
	mix-blend-mode: screen;
	opacity: calc(var(--vhs-grain) * var(--vhs-wear));
}

/* ── head-switching noise band — PERMANENT structural constant of VHS
   playback (recipe point 6), not a burst. Sits flush to the very bottom
   edge; the timecode block floats just above it so they never collide.
   Redrawn on its own interval (vhs.js) — short scrambled horizontal
   segments, most rows untouched so it reads as broken scanlines. ───────── */
#vhs-headswitch {
	position: absolute; left: 0; right: 0; bottom: 0;
	width: 100%; height: var(--vhs-headswitch-h);
	mix-blend-mode: screen;
	opacity: calc(var(--vhs-headswitch) * var(--vhs-wear));
	/* the scrambled bottom-edge scanline segments read too crisp/hard-edged
	   at native canvas-pixel sharpness — a touch of blur softens them into
	   the same soft-analog character as the rest of the effect. */
	filter: blur(1px);
}

/* ── soft undulating side borders — Jordan's third pass on this element:
   (1) shake -> real traveling-wave undulation, (2) more amplitude/speed,
   NO jagged edges, a bit of blur. The clip-path below is now just a
   flat, non-jagged rest shape (a straight inset line, matching vhs.js's
   synthetic baseX for each side) — the pre-JS/no-JS fallback and what
   stopBorderWiggle() falls back to on Off. vhs.js no longer reads this
   back out; it generates its own smooth evenly-spaced point set and
   drives the live animated version via inline style.clipPath, so this
   CSS shape and the animated one are DELIBERATELY different (a static
   flat inset vs. a moving wave) rather than the old "CSS is the single
   source of truth" arrangement, since there's no meaningful shared
   geometry left to keep in sync once the jaggedness is gone. The blur
   softens the edge itself, reinforcing "no jagged edges" and asked for
   on its own ("a bit blurry"). ────────────────────────────────────────── */
/* TWO elements, not one — a real CSS gotcha caught from Jordan's screenshot
   ("they look too crisp" even with blur+drop-shadow already in the CSS):
   filter and clip-path on the SAME element both apply to that element's own
   box, in filter-then-clip order, so the clip-path crops away exactly the
   part of the filter (blur bleed, drop-shadow fringe) that would extend
   PAST the shape's boundary — the one place it needs to be visible. Net
   effect: a filter that's computed but invisible, reading as crisp no
   matter how large blur/aberration values get. Fix: .vhs-border-fx (outer,
   sized/positioned, carries the filter, NO clip-path of its own) wraps
   .vhs-border (inner, inset:0, carries the clip-path + background) — the
   filter now applies to an already-clipped child with nothing above it
   clipping the filter's own bleed. */
.vhs-border-fx {
	position: absolute;
	/* extends PAST the viewport top/bottom on purpose — the wave's two
	   anchor points (pinned, no glow) sit at 0%/100% of THIS box, and
	   with top/bottom:0 that put them exactly at the screen edge, so the
	   white glow's natural transition into the anchor was visibly
	   clipped right at the top/bottom of the page. Pushing the box (and
	   therefore both anchors) off-screen means the visible viewport only
	   ever shows a middle slice of continuous wave — no anchor, no
	   transition, no clipped glow, ever in view. */
	top: -60px; bottom: -60px;
	width: var(--vhs-border);
	/* blur softens the edge itself; the two drop-shadows trace a glowing
	   fringe ALONG the moving silhouette (not the flat global CA on
	   #vellum-main) — a drop-shadow of a clipped shape is a same-alpha
	   colored copy offset in x, so it follows the wave exactly, no matter
	   how the clip-path is animated. Order matters: blur first so the
	   fringe copies are soft too, not crisp color edges on a blurred base.
	   WHITE, not the brand blue — Jordan's reference image (a real VHS-
	   style black/wavy-edge-into-color transition) showed a white glow
	   right at the boundary, not a color-matched one; a rim-light reads
	   as "worn tape edge," a blue-tinted one read as branding. Same color
	   both directions (not a directional red/cyan split, which was an
	   earlier mirroring bug — see basedin3-vhs-camcorder-followup log)
	   so it's identical on both the left and right strip regardless of
	   which way is "inward." */
	filter:
		blur(var(--vhs-border-blur))
		drop-shadow(var(--vhs-border-ca) 0 0 rgba(255, 255, 255, .8))
		drop-shadow(calc(var(--vhs-border-ca) * -1) 0 0 rgba(255, 255, 255, .8));
}
.vhs-border-fx.left { left: 0; }
.vhs-border-fx.right { right: 0; }
/* the glow filter on .vhs-border-fx wraps the WHOLE silhouette, including
   the strip's flat OUTER edge (flush with the true browser edge) — not
   just the wavy inner boundary facing the content. That outer edge never
   moves, so its glow read as an unwanted straight white line pinned to
   the very side of the screen. Rather than try to reshape the wave's own
   percentage math to push that straight edge off-screen (fragile: it's
   a responsive vw-based width, and the wavy/glow math is all relative to
   it), a plain opaque sliver painted on top, no filter of its own, just
   covers that outer strip and hides the artifact directly. 35% of the
   border's own width comfortably clears the wave's closest approach to
   the edge (worst case ~47% of the strip width at max Tracking rough-
   ness) with real margin to spare. */
.vhs-border-mask {
	position: absolute; top: 0; bottom: 0;
	width: calc(var(--vhs-border) * 0.35);
	background: #050505;
	z-index: 1;
}
.vhs-border-mask.left { left: 0; }
.vhs-border-mask.right { right: 0; }
:root.vhs-off .vhs-border-mask { opacity: 0 !important; }
.vhs-border {
	position: absolute; inset: 0;
	background: #050505;
	/* opacity is intentionally NOT tied to --vhs-wear — Jordan's correction:
	   raising Tracking should straighten the wiggle out (already handled,
	   amplitude scales with state.wear in vhs.js), not fade the border
	   away. The border stays fully opaque across the whole Tracking range;
	   only :root.vhs-off below (a real, separate state — the Off toggle,
	   not the slider) hides it. */
	opacity: 1;
}
.vhs-border.left {
	clip-path: polygon(0% 0%, 55% 0%, 55% 100%, 0% 100%);
	transform-origin: right center;
}
.vhs-border.right {
	clip-path: polygon(100% 0%, 45% 0%, 45% 100%, 100% 100%);
	transform-origin: left center;
}
/* ── highlight-biased tint (recipe point 3) — soft-light blend only
   pushes midtones/highlights toward the pale green/cream; shadows and
   warm reds pass through untouched, which is what the reference actually
   shows (a flat hue-rotate/sepia would gray the reds out too — see the
   recipe's gotchas). ─────────────────────────────────────────────────── */
.vhs-grade {
	position: absolute; inset: 0;
	background: radial-gradient(120% 90% at 50% 38%,
		rgba(214, 224, 178, calc(var(--vhs-grade) * var(--vhs-wear))) 0%,
		rgba(214, 224, 178, 0) 64%);
	mix-blend-mode: soft-light;
}

/* ── damage-burst layer — dropout streaks spawned/removed by vhs.js,
   inert (empty, zero cost) except during a burst window. ────────────────── */
.vhs-burst-layer {
	position: absolute; inset: 0;
	overflow: hidden;
}
.vhs-dropout {
	position: absolute; left: 0; right: 0;
	background: linear-gradient(to bottom,
		rgba(255,255,255,0) 0%, rgba(255,255,255,.92) 46%,
		rgba(255,255,255,.92) 54%, rgba(255,255,255,0) 100%);
	opacity: 0;
	transition: opacity 70ms linear;
	/* burst dropout streaks read too clean/hard-edged at full sharpness —
	   a touch of blur, matching the head-switch band treatment above,
	   keeps every "stray horizontal line" moment soft and analog rather
	   than a crisp vector rectangle flashing on screen. */
	filter: blur(1.2px);
}
.vhs-dropout.is-on { opacity: calc(var(--vhs-burst-amt) * var(--vhs-wear)); }

/* ── timecode — the signature element (recipe point 1). DOM, not canvas:
   needs to stay crisp, and it's cheap. Colons run slightly dimmer than
   digits, matching the reference readout. Positioned to clear the
   head-switch band with a fixed margin so they never visually collide. ── */
.vhs-timecode {
	position: fixed;
	left: 50%;
	bottom: calc(var(--vhs-headswitch-h) + 10px);
	transform: translateX(-50%);
	z-index: 401;
	display: flex;
	align-items: center;
	background: #000;
	border: 1px solid rgba(255,255,255,.75);
	padding: 4px 10px;
	font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
	font-size: 15px;
	letter-spacing: .04em;
	color: #fff;
	text-shadow: 0 0 3px rgba(255,255,255,.4);
	opacity: calc(var(--vhs-timecode-opacity) * var(--vhs-wear));
	white-space: nowrap;
	box-sizing: border-box;
}
/* Desktop: moved off dead-center to the left edge, matching the Tape
   controls box's own left/right margin (20px) on the opposite corner —
   and boxed to the exact same height as that controls box (both read
   --vhs-box-h; see the controls-box rule further down) rather than
   sizing to its own shorter single-line content, so the two boxes read
   as a matched pair at a glance instead of two different heights.
   Mobile untouched — stays centered, clearing the sticky CTA as before. */
@media (min-width: 768px) {
	.vhs-timecode {
		left: 20px;
		right: auto;
		transform: none;
		height: var(--vhs-box-h);
		box-sizing: border-box;
	}
}
.vhs-tc-colon { color: rgba(255,255,255,.55); margin: 0 1px; }
/* the sticky "Book a Tour" bar (style.css .sticky-cta) is a FULL-WIDTH
   82px bar on phones (bottom:0;left:0;right:0 — not a corner button the
   way the design originally assumed), so anything else pinned to the
   bottom on mobile needs to clear ABOVE it or it renders on top and
   covers the CTA's own text (z-index 401 vs the CTA's 50). Caught from
   Jordan's screenshot: the timecode block was doing exactly that. */
@media (max-width: 767px) {
	.vhs-timecode { bottom: calc(var(--sticky-cta-h, 82px) + var(--vhs-headswitch-h) + 10px); }
}
@media (max-width: 480px) {
	.vhs-timecode { font-size: 12px; padding: 3px 7px; bottom: calc(var(--sticky-cta-h, 82px) + var(--vhs-headswitch-h) + 8px); }
}

/* ── Off toggle — the reachable, genuinely-clean state ───────────────────
   :root.vhs-off zeroes --vhs-wear (handled by vhs.js setting the CSS
   custom property), which alone brings every calc()-driven opacity/filter
   term above to its neutral value. What --vhs-wear can't reach on its own
   — because these are JS interval loops and CSS keyframe animations, not
   continuous functions of a single scalar — gets its own explicit stop
   right here, so nothing keeps moving under an invisible layer:
     - #vhs-grain / #vhs-headswitch: vhs.js clears their setInterval AND
       these rules pause+hide, belt and suspenders.
     - .vhs-border's undulation: vhs.js cancels its own requestAnimationFrame
       loop AND clears the inline style.clipPath override it was writing,
       falling back to this file's static base shape underneath.
     - .vhs-timecode: vhs.js clears its own tick interval (digits freeze).
     - burst scheduler: vhs.js clears the pending setTimeout AND checks
       the off-flag at the top of the fire callback (a timer armed a few
       ms before the click can still fire once — guard both ends).
   This is the exact bug crt.css shipped once (a CSS-animated band that
   Off only hid via opacity, not animation-play-state, kept sliding
   underneath) — audited against every animation/interval this file adds. */
:root.vhs-off #vhs-grain,
:root.vhs-off #vhs-headswitch {
	opacity: 0 !important;
	animation-play-state: paused;
}
:root.vhs-off .vhs-border {
	/* the undulation loop is stopped in JS (stopBorderWiggle, cancels its
	   own rAF + clears the inline clip-path override) — see the comment
	   on .vhs-border above. This opacity rule is belt-and-suspenders. */
	opacity: 0 !important;
}
:root.vhs-off .vhs-grade,
:root.vhs-off .vhs-timecode,
:root.vhs-off .vhs-dropout {
	opacity: 0 !important;
}

@media (prefers-reduced-motion: reduce) {
	/* .vhs-border: vhs.js's PRM branch never calls startBorderWiggle() at
	   all (double-guarded — the boot sequence skips it under PRM, and the
	   function itself also checks reduceMotion), so the border just shows
	   this file's static base clip-path, untouched — the jagged geometry
	   stays visible (it's a look) but nothing animates it. */
	/* grain/headswitch: vhs.js draws exactly one static frame then stops
	   its interval under PRM (see reduced-motion branch) — the texture
	   stays visible (it's a look, not motion) but nothing ticks. Burst
	   scheduler never arms under PRM at all (no bursts). Timecode: vhs.js
	   also skips starting its tick interval under PRM, freezing on the
	   seeded start value — a still readout, not a moving one. */
}

/* ── control widget — the ONE interactive element, a sibling of
   .vhs-glass (never a child), so the glass stack's pointer-events:none
   never needs a hole punched in it. Desktop: fixed bottom-right, always
   expanded. Mobile: compact tab that expands on tap (left side, clear of
   the sticky "Book a tour" CTA which owns the bottom-right on phones). ── */
.vhs-controls {
	position: fixed;
	z-index: 402;
	font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.vhs-controls__bar {
	display: flex;
	align-items: center;
	gap: 10px;
	background: rgba(5, 5, 5, .88);
	border: 1px solid rgba(30, 131, 237, .4);
	/* Uniform padding. Was 9px 14px, which with the 1px border gave 10px
	   of space above and below the buttons but 15px either side — visibly
	   wider than tall once the bar collapses to just the two buttons.
	   Edited here rather than overridden from style.css because vhs.css
	   is enqueued AFTER the child stylesheet, so an equal-specificity
	   override there would lose on source order. */
	padding: 9px;
	-webkit-backdrop-filter: blur(6px);
	backdrop-filter: blur(6px);
}
.vhs-controls__label {
	font-size: 10px; letter-spacing: .2em; text-transform: uppercase;
	color: rgba(255, 255, 255, .55); white-space: nowrap;
}
.vhs-controls input[type="range"] {
	width: 110px; accent-color: var(--color-accent); background: transparent; border: 0; padding: 0;
}
.vhs-controls button {
	font-family: inherit; font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
	background: transparent; color: #fff; border: 1px solid rgba(255, 255, 255, .35);
	padding: 7px 10px; cursor: pointer; white-space: nowrap;
}
.vhs-controls button:hover,
.vhs-controls button:focus-visible { background: var(--color-accent); border-color: var(--color-accent); color: #fff; }
.vhs-controls input[type="range"]:disabled { opacity: .35; cursor: not-allowed; }
.vhs-controls.is-off #vhs-off-toggle { background: var(--color-accent); border-color: var(--color-accent); }
/* play/pause is an inline SVG icon, not a text glyph — iOS swaps Unicode
   symbols like ▶/⏸ for Apple's own colorful emoji glyphs, which breaks the
   plain VCR-button look, so every icon on this site is drawn as SVG instead. */
/* #vhs-theme-toggle shares this exact recipe with #vhs-off-toggle — not
   just the same values duplicated — so the two buttons are guaranteed
   pixel-identical in height, and the theme toggle can't shift height
   between light/dark clicks either: display:flex + align-items:center
   means the button's own box height comes from ITS padding, not from
   whichever icon happens to be visible inside it (the actual cause of
   the reported height jump — see the icon rules further down, which
   switched to display:block/none for the same reason). */
#vhs-off-toggle, #vhs-theme-toggle { display: flex; align-items: center; justify-content: center; padding: 8px 11px; }
.vhs-icon { display: block; color: currentColor; }
.vhs-controls__toggle { display: none; }

@media (min-width: 768px) {
	.vhs-controls { right: 20px; bottom: 20px; }
	/* box-sizing: border-box so the shared --vhs-box-h includes this box's
	   own 1px border, exactly like the timecode box above — otherwise the
	   two would be 2px off from each other (border added on TOP of height
	   under the default content-box model). */
	.vhs-controls__bar { height: var(--vhs-box-h); box-sizing: border-box; }
}
@media (max-width: 767px) {
	/* was bottom:12px, sitting well inside the sticky CTA's 82px band —
	   see the matching comment on .vhs-timecode above, same root cause. */
	.vhs-controls { left: 12px; bottom: calc(var(--sticky-cta-h, 82px) + 12px); }
	.vhs-controls__toggle {
		display: block;
		font-family: inherit; font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
		background: rgba(5,5,5,.88); color: #fff; border: 1px solid rgba(30, 131, 237, .4);
		padding: 8px 12px; cursor: pointer;
	}
	.vhs-controls__bar {
		position: absolute; left: 0; bottom: calc(100% + 8px);
		flex-direction: column; align-items: stretch; gap: 8px;
		display: none;
		min-width: 168px;
	}
	.vhs-controls__bar.is-open { display: flex; }
}
