/* ==========================================================================
 * Path:        public_html/assets/css/hero.css
 * Filename:    hero.css
 * Project:     revisualized.com
 * Description: Full-bleed three-layer hero band.
 * Status:      production
 * Revision:    5
 * Updated:     2026-08-11 (Rev 5: travel raised to 220 on both stencils and
 *              200 on the base. Base is now the tightest hard constraint in
 *              the set at 108.9 of 120 vertical, and front now depends on
 *              its own ink clearance in the 700 to 827px window. Both are
 *              recorded under THE NARROW DESKTOP WINDOW below. Mobile
 *              retuned: base and front sit strictly inside the box budget
 *              there, mid alone carries the exception.
 *              Rev 4: parallax is now angled rather than
 *              vertical. Added --hero_*_angle beside each --hero_*_travel;
 *              travel is now a vector magnitude, not a vertical distance.
 *              Rewrote the travel budget section to separate the box budget
 *              from the ink budget, which is what actually governs the two
 *              stencil layers. Tuned tint and stencil opacity for the
 *              replacement overlay art. Rev 3: the negative margin is now
 *              opt-in via "section .hero_band". Rev 2: per-slug base rules
 *              removed; the base is an <img> emitted by render_hero_band.
 *              Added --hero_base_filter so the base responds to the theme.)
 * Requires:    main.css (theme tokens)
 * Included by: includes/header.php
 * Driven by:   assets/js/main.js, which reads the tokens below at runtime.
 *              The numbers live here and only here. Do not duplicate them
 *              into the script.
 *
 * NOT GENERATED. Fixed size regardless of how many articles or projects
 * exist. Revision 1 declared one rule per slug here, which meant every
 * visitor downloaded every rule to use one: at 500 pieces a 103 KB
 * stylesheet, 0.19 percent of it applicable. The base is now an <img> whose
 * src PHP resolves per page. The two overlays are shared site-wide, so they
 * stay here as exactly two rules.
 *
 * LAYER MODEL
 *   base   opaque JPEG, per page, lives with its content type
 *   tint   flat colour over the base, shifts it bluer or whiter per theme
 *   mid    shared SVG used as a mask; colour comes from the theme
 *   front  shared SVG used as a mask; nearer, denser
 *
 * THE TRAVEL BUDGET, WHICH THE EARLIER ATTEMPT GOT WRONG
 *   Each layer is 150% of the band and centred, so it overhangs by
 *   (1.5 - 1) / 2 = 25% of the band on every side. At a 480px band that is
 *   exactly 120px of vertical headroom.
 *
 *   The earlier build drove translation off raw scrollY at speeds up to 0.85,
 *   which moved the front layer 408px over a single band height. It blew the
 *   budget by 3.4x. That was the edge exposure.
 *
 *   Drive translation from the band's progress through the viewport,
 *   normalised 0 to 1, never from scrollY:
 *
 *     progress = clamp((viewportBottom - bandTop) / (viewportH + bandH), 0, 1)
 *     offset   = (progress - 0.5) * 2 * travel
 *
 *   At progress 0 and 1 the layer sits at exactly plus or minus travel, so it
 *   cannot drift further at any scroll length or viewport size. Compose onto
 *   the centring transform:
 *
 *     transform: translate(-50%, -50%) translate3d(Xpx, Ypx, 0);
 *
 * TRAVEL IS NOW A MAGNITUDE, NOT A DISTANCE UP
 *   Rev 3 moved every layer straight up. Rev 4 gives each one a direction,
 *   so travel splits across both axes:
 *
 *     vertical spend   = travel * abs(sin(angle))
 *     horizontal spend = travel * abs(cos(angle))
 *
 *   Angle convention, matching CSS rotate() being clockwise and screen y
 *   growing downward: 0 = right, 90 = up, 180 = left, 270 = down.
 *   Counterclockwise. The script negates the sine on the way out.
 *
 *   The vertical budget is 25% of band height. The horizontal budget is 25%
 *   of band width, which on any desktop viewport is several times larger, so
 *   vertical is nearly always the binding constraint. On a phone that
 *   reverses: at a 320px viewport the horizontal budget is 80px against a
 *   75px vertical one, and a near-horizontal angle becomes the tight one.
 *   The mobile block below is set for the 320px case, not the 700px case.
 *
 * BOX BUDGET VERSUS INK BUDGET, WHICH IS WHY MID EXCEEDS 120 ON PURPOSE
 *   Everything above governs the layer box. It is the correct and only
 *   budget for the base, whose JPEG fills its box corner to corner: move the
 *   box out of the band and you see the gap immediately.
 *
 *   Neither stencil fills its box. They are alpha masks with empty margins,
 *   so the box edge can enter the frame without anything visible happening,
 *   because there is nothing drawn near it. Measured against the deployed
 *   hero_overlay_mid.svg at a 2180px band:
 *
 *     ink bounding box   x 891 to 1987, y 445 to 755  of a 2880x1200 canvas
 *     layer box          3270 x 720, mask cover shows SVG rows 283 to 917
 *     ink within box     rows 184 to 536 of 720
 *     clearance          184px above the ink, 184px below it
 *
 *   So mid carries 184px of slack past the 120px box budget before its box
 *   edge could reach its own artwork, and 150px of travel at 110 degrees
 *   spends 141px vertically. That is inside the ink budget with room, and it
 *   is why the value below exceeds the stated limit deliberately.
 *
 *   THIS IS COUPLED TO THE ASSET. The clearance is a property of that
 *   specific file, not of the mid layer. Replace hero_overlay_mid.svg, or
 *   regenerate it with scripts/generate_hero_images.py, and the slack is
 *   gone: the generator draws to the canvas edges, which puts ink hard
 *   against the box edge and makes 120 the real ceiling again. If you swap
 *   that file, drop --hero_mid_travel to 115 in the same commit.
 *
 * THE NARROW DESKTOP WINDOW, 700px TO 827px
 *   The mobile query fires at 700px, so between 700 and 827 the desktop
 *   values below are live on a band too narrow to afford one of them.
 *   Front at 220px and 160 degrees spends 206.7px horizontally, and the
 *   horizontal box budget is 25% of band width, so it needs a band of at
 *   least 826.9px. A tablet held in portrait at 768px lands inside that gap.
 *
 *   Front survives it on ink clearance, the same way mid survives its
 *   vertical overrun. Measured from the deployed hero_overlay_front.svg:
 *
 *     ink bounding box   x -113 to 933 of a 2880 wide canvas, no wrapping
 *                        transform, so everything left of 0 is clipped by
 *                        the viewBox and the usable ink is x 0 to 933
 *     empty margin       1947 of 2880 units on the right, 68% of the canvas
 *
 *   Front travels leftward at 160 degrees, which pulls its right box edge
 *   into the frame. That edge has 68% of the canvas worth of nothing behind
 *   it, so the overrun exposes empty mask rather than a cut edge.
 *
 *   THIS IS THE THIRD ASSET-COUPLED EXCEPTION IN THIS FILE and the least
 *   comfortable of them, because it depends on front's artwork staying
 *   left-anchored. If hero_overlay_front.svg is ever redrawn full-bleed,
 *   this stops being free: either drop --hero_front_travel to 175, which
 *   holds the box budget down to a 700px band, or move the breakpoint in the
 *   media query up to 830px.
 *
 *   Base carries no exception and cannot be given one, because its JPEG
 *   fills the box corner to corner. At 200px and 33 degrees it spends
 *   108.9px vertically against 120, and 167.7px horizontally, which needs a
 *   band of at least 670.9px. The breakpoint at 700px clears that by 29px of
 *   band width. Both margins are under 10 percent, so base is the number to
 *   recheck first if the band height or the 150% layer size ever changes.
 * ========================================================================== */

/* The negative margin exists only to cancel the 32px/20px padding on a
   wrapping section. On the homepage the band is a direct child of <main>,
   which carries no padding at all, so applying it there pushed the band 40px
   wider than the viewport and 32px up into the nav. Default to no negative
   margin; opt in only when a section is actually above it. */
.hero_band {
  position: relative;
  margin: 0 0 28px;
  height: var(--hero_band_height);
  overflow: hidden;
  isolation: isolate;
  background-color: var(--terminal-background);
  border-bottom: 1px solid var(--color-border-separator);
}

/* about/index.php puts the band directly inside .about; both detail templates
   put it inside <article> within .articles or .projects. A descendant match
   covers both without either template knowing about its own padding. */
section .hero_band {
  margin: -32px -20px 28px;
}

.hero_band__layer,
.hero_band__tint {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 150%;
  height: 150%;
  transform: translate(-50%, -50%);
  will-change: transform;
  pointer-events: none;
}

/* The base is an <img>, so it needs object-fit rather than background-size.
   The filter is how the base responds to the theme. Per-theme base files
   would triple storage for the same effect. invert plus a 180 degree hue
   rotation flips luminance while holding hue, which is what turns a dark
   cobalt field into a light one without a second file. */
.hero_band__layer--base {
  object-fit: cover;
  object-position: center;
  filter: var(--hero_base_filter);
}

/* The tint does not translate. It is a flat wash sized to cover the band,
   and moving it would drag a hard colour edge across the frame for nothing.
   main.js skips it deliberately. */
.hero_band__tint {
  background-color: var(--hero_tint_color);
  opacity: var(--hero_tint_opacity);
  mix-blend-mode: var(--hero_tint_blend);
}

.hero_band__layer--mid,
.hero_band__layer--front {
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-size: cover;
          mask-size: cover;
}

.hero_band__layer--mid {
  -webkit-mask-image: url("/assets/images/heroes/hero_overlay_mid.svg");
          mask-image: url("/assets/images/heroes/hero_overlay_mid.svg");
  background-color: var(--hero_mid_color);
  opacity: var(--hero_mid_opacity);
}

.hero_band__layer--front {
  -webkit-mask-image: url("/assets/images/heroes/hero_overlay_front.svg");
          mask-image: url("/assets/images/heroes/hero_overlay_front.svg");
  background-color: var(--hero_front_color);
  opacity: var(--hero_front_opacity);
}

/* Theme tokens. Tune these, never the image files.
   main.js reads every travel and angle value below through
   getComputedStyle, so the media query further down retunes the motion for
   small screens without the script knowing a breakpoint exists. */
:root,
html[data-theme="dark"] {
  --hero_band_height:   480px;
  --hero_travel_limit:  120px;  /* 25% of the band. See the ink budget note. */

  /* Vertical budget is 120px here. Horizontal is 25% of band width, so it
     is stated as the narrowest band each value can run on. */
  --hero_base_travel:   200px;  /* 33deg  -> vert 108.9 of 120, horiz 167.7, needs 671px */
  --hero_base_angle:     33deg;
  --hero_mid_travel:    220px;  /* 110deg -> vert 206.7, ink budget 304, horiz 75.2 */
  --hero_mid_angle:     110deg;
  --hero_front_travel:  220px;  /* 160deg -> vert  75.2 of 120, horiz 206.7, needs 827px */
  --hero_front_angle:   160deg;

  --hero_base_filter:   none;
  --hero_tint_color:    #1b2a63;
  --hero_tint_opacity:  0.50;
  --hero_tint_blend:    color;
  --hero_mid_color:     #79c0ff;
  --hero_mid_opacity:   0.69;
  --hero_front_color:   #ff7878;
  --hero_front_opacity: 0.69;
}

/* The stencil opacities in the two blocks below are inherited from Rev 3 and
   were set against the previous overlay art. The current mid stencil is a
   single fully opaque path, so it reads far stronger per unit of opacity
   than the sparse art these values were chosen for. Both need the same
   tuning pass the dark block just had. */
html[data-theme="light"] {
  --hero_base_filter:   invert(1) hue-rotate(180deg) saturate(0.85) brightness(1.05);
  --hero_tint_color:    #ffffff;
  --hero_tint_opacity:  0.72;
  --hero_tint_blend:    screen;
  --hero_mid_color:     #0b4a9e;
  --hero_mid_opacity:   0.26;
  --hero_front_color:   #8c1a3c;
  --hero_front_opacity: 0.30;
}

html[data-theme="monochrome"] {
  --hero_base_filter:   grayscale(1) contrast(1.1);
  --hero_tint_color:    #101010;
  --hero_tint_opacity:  0.55;
  --hero_tint_blend:    color;
  --hero_mid_color:     #dedede;
  --hero_mid_opacity:   0.22;
  --hero_front_color:   #f0f0f0;
  --hero_front_opacity: 0.34;
}

/* 300px band, so the vertical budget drops to 75px. The horizontal budget
   drops much harder, to 25% of viewport width, and it keeps dropping: 80px
   at 320px, 75px at 300px. Set against 300px rather than 320px, because a
   folding phone's cover screen is the narrowest thing that will ever load
   this and it costs almost nothing to clear it.

   Front and base are held strictly inside the box budget here, so neither
   depends on artwork that could be redrawn. Mid alone carries the ink
   exception, and its clearance measures 167px at this band height, giving
   an effective vertical ceiling of 242px against the 128.7px it spends. */
@media (max-width: 700px) {
  :root {
    --hero_band_height:  300px;
    --hero_travel_limit:  75px;

    --hero_base_travel:   88px;  /* 33deg  -> vert  47.9 of 75, horiz 73.8 of 75 */
    --hero_mid_travel:   137px;  /* 110deg -> vert 128.7, ink budget 242, horiz 46.9 */
    --hero_front_travel:  78px;  /* 160deg -> vert  26.7 of 75, horiz 73.3 of 75 */
  }
  .hero_band { margin: 0 0 22px; }
  section .hero_band { margin: -32px -20px 22px; }
}

/* Backstop, not the primary control. main.js checks the same query and never
   writes a transform when it matches, but an author !important declaration
   also outranks the inline style the script would set, so the band still
   comes to rest correctly if the script runs before the preference is read
   or if a future edit drops the guard. */
@media (prefers-reduced-motion: reduce) {
  .hero_band__layer,
  .hero_band__tint { transform: translate(-50%, -50%) !important; }
}
