前端嘛 Logo
前端嘛
创意网页设计

创意网页设计

2026-06-01
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Six Faces / Walking The Cow</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link
      href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:wght@300;400&display=swap"
      rel="stylesheet"
    />
    <style>
      @import url("https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:wght@300;400&display=swap");

      @layer reset, tokens, base, layout, cube, ui, cards, reveal, theme, responsive;

      @layer reset {
        *,
        *::before,
        *::after {
          box-sizing: border-box;
          margin: 0;
          padding: 0;
        }
      }

      @layer tokens {
        :root {
          color-scheme: dark;

          --dark-bg: #1c1814;
          --dark-fg: #ede8df;
          --dark-muted: #8a7b6e;
          --light-bg: #f0ece3;
          --light-fg: #0d0d14;
          --light-muted: #9a9aaa;
          --accent-dark: #d4a84b;
          --accent-light: #3a6e00;

          --bg: var(--dark-bg);
          --fg: var(--dark-fg);
          --muted: var(--dark-muted);
          --accent: var(--accent-dark);

          --font-display: "Bebas Neue", sans-serif;
          --font-mono: "DM Mono", monospace;
          --hairline: 0.0625rem;
          --ui-inset: 2rem;
          --card-bg: rgba(28, 24, 20, 0.82);
          --card-border: rgba(212, 168, 75, 0.2);
          --nav-x: calc(var(--ui-inset) + 0.125rem);
          --reveal-offset: 0.625rem;
          --reveal-duration: 0.5s;
          --z-ui: 10;
        }
      }

      @layer base {
        html {
          color-scheme: dark;
        }

        body {
          background: var(--bg);
          color: var(--fg);
          font-family: var(--font-mono);
          overflow-x: hidden;
          transition:
            background 0.3s ease,
            color 0.3s ease;
        }
      }

      @layer layout {
        #scene {
          position: fixed;
          inset: 0;
          z-index: 0;
          display: flex;
          align-items: center;
          justify-content: center;
          perspective: 1100px;
          pointer-events: none;
        }

        #scroll_container {
          position: relative;
          z-index: 1;
        }

        section {
          min-height: 100vh;
          display: flex;
          align-items: center;
          padding: 6rem calc(5rem + var(--ui-inset)) 6rem 5rem;
        }
      }

      @layer cube {
        #cube {
          --s: min(74vw, 74vh, 560px);
          width: var(--s);
          height: var(--s);
          position: relative;
          transform-style: preserve-3d;
          transform: rotateX(90deg) rotateY(0deg);
          will-change: transform;
        }

        .face {
          position: absolute;
          inset: 0;
          overflow: hidden;
          backface-visibility: hidden;
          background:
            repeating-linear-gradient(
              0deg,
              rgba(255, 255, 255, 0.02) 0,
              rgba(255, 255, 255, 0.02) 1px,
              transparent 1px,
              transparent 48px
            ),
            repeating-linear-gradient(
              90deg,
              rgba(255, 255, 255, 0.02) 0,
              rgba(255, 255, 255, 0.02) 1px,
              transparent 1px,
              transparent 48px
            ),
            #14100d;

          img {
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
            object-fit: cover;
            display: block;
          }

          &:has(img) .face-ph {
            display: none;
          }
        }

        .face-ph {
          position: absolute;
          bottom: 1.5rem;
          left: 1.75rem;
          font-family: var(--font-display);
          font-size: clamp(2rem, 8vw, 5rem);
          letter-spacing: 0.04em;
          color: rgba(255, 255, 255, 0.06);
          pointer-events: none;
          user-select: none;
        }

        .face[data-face="front"] {
          transform: translateZ(calc(var(--s) / 2));
        }
        .face[data-face="back"] {
          transform: rotateY(180deg) translateZ(calc(var(--s) / 2));
        }
        .face[data-face="right"] {
          transform: rotateY(90deg) translateZ(calc(var(--s) / 2));
        }
        .face[data-face="left"] {
          transform: rotateY(-90deg) translateZ(calc(var(--s) / 2));
        }
        .face[data-face="top"] {
          transform: rotateX(-90deg) translateZ(calc(var(--s) / 2));
        }
        .face[data-face="bottom"] {
          transform: rotateX(90deg) translateZ(calc(var(--s) / 2));
        }
      }

      @layer ui {
        #hud {
          position: fixed;
          top: var(--ui-inset);
          right: var(--ui-inset);
          z-index: var(--z-ui);
          text-align: right;
          font-size: 0.65rem;
          letter-spacing: 0.15em;
          color: var(--muted);
          text-transform: uppercase;

          .progress-bar {
            width: 7.5rem;
            height: var(--hairline);
            background: var(--muted);
            margin-block-start: 0.5rem;
            margin-inline-start: auto;
            position: relative;
            overflow: hidden;
          }

          .progress-fill {
            position: absolute;
            inset-block: 0;
            inset-inline-start: 0;
            width: 0%;
            background: var(--accent);
            transition: width 0.1s linear;
          }

          .scene-label {
            font-size: 0.6rem;
            color: var(--accent);
            margin-block-start: 0.4rem;
          }
        }

        #scene_strip {
          position: fixed;
          left: var(--nav-x);
          top: 50%;
          translate: -50% -50%;
          z-index: var(--z-ui);
          display: flex;
          flex-direction: column;
          gap: 0.75rem;
        }

        .scene-dot {
          position: relative;
          display: block;
          width: 0.25rem;
          height: 0.25rem;
          border-radius: 50%;
          background: var(--muted);
          transition:
            background 0.3s,
            scale 0.3s;
          cursor: pointer;

          &::before {
            content: "";
            position: absolute;
            inset: -0.2rem;
          }

          &.active {
            background: var(--accent);
            scale: 1.8;
          }
        }

        #theme_toggle {
          position: fixed;
          bottom: var(--ui-inset);
          left: var(--nav-x);
          translate: -50% 0;
          z-index: var(--z-ui);
          width: 2rem;
          height: 2rem;
          border: none;
          background: color-mix(in srgb, var(--muted) 35%, transparent);
          border-radius: 50%;
          cursor: pointer;
          display: flex;
          align-items: center;
          justify-content: center;
          transition: background 0.3s;

          &:hover {
            background: color-mix(in srgb, var(--muted) 55%, transparent);
          }

          svg {
            width: 0.875rem;
            height: 0.875rem;
            position: absolute;
            transition:
              opacity 0.3s ease,
              rotate 0.3s ease;
            color: var(--accent);
          }

          .icon-sun {
            opacity: 1;
            rotate: 0deg;
          }

          .icon-moon {
            opacity: 0;
            rotate: 90deg;
          }
        }

        #face_caption {
          position: fixed;
          bottom: var(--ui-inset);
          left: 50%;
          translate: -50% 0;
          z-index: var(--z-ui);
          text-align: center;
          pointer-events: none;
          user-select: none;
        }

        #face_caption_num {
          font-size: 0.58rem;
          letter-spacing: 0.28em;
          color: var(--accent);
          text-transform: uppercase;
          margin-block-end: 0.15rem;
        }

        #face_caption_name {
          font-family: var(--font-display);
          font-size: clamp(1.8rem, 5vw, 3.5rem);
          letter-spacing: 0.08em;
          color: var(--muted);
          opacity: 0.5;
          line-height: 1;
        }

        #credit {
          position: fixed;
          right: var(--ui-inset);
          top: 50%;
          transform: translateY(-50%) rotate(-90deg);
          transform-origin: right center;
          z-index: var(--z-ui);
          font-family: var(--font-mono);
          font-size: 0.65rem;
          letter-spacing: 0.15em;
          text-transform: uppercase;

          a {
            color: var(--muted);
            text-decoration: none;
          }
        }
      }

      @layer cards {
        .text-card {
          max-width: 23.75rem;
          padding: 2.25rem 2rem;
          background: var(--card-bg);
          border-left: var(--hairline) solid var(--card-border);
          backdrop-filter: blur(6px) saturate(120%);
          -webkit-backdrop-filter: blur(6px) saturate(120%);
          overflow: hidden;
          transition:
            background 0.3s ease,
            border-color 0.3s ease;

          &.right {
            margin-inline-start: auto;
            border-left: none;
            border-right: var(--hairline) solid var(--card-border);
            text-align: right;

            .h-line {
              transform-origin: right;
              margin-inline-start: auto;
            }
          }

          &.center {
            margin-inline: auto;
            border-left: none;
            border-top: var(--hairline) solid var(--card-border);
            text-align: center;
            max-width: 28.75rem;

            .h-line {
              transform-origin: center;
              margin-inline: auto;
            }
          }
        }

        .tag {
          font-size: 0.6rem;
          letter-spacing: 0.25em;
          text-transform: uppercase;
          color: var(--accent);
          margin-block-end: 1.1rem;
        }

        :where(h1, h2) {
          font-family: var(--font-display);
          font-weight: 400;
          letter-spacing: 0.03em;
          line-height: 0.92;
        }

        h1 {
          font-size: clamp(3rem, 8vw, 6.5rem);
        }
        h2 {
          font-size: clamp(2.2rem, 5vw, 4rem);
        }

        .body-text {
          font-size: 0.78rem;
          line-height: 1.8;
          color: color-mix(in srgb, var(--fg) 55%, transparent);
          margin-block-start: 1.25rem;
        }

        .stat-row {
          display: flex;
          gap: 2.5rem;
          margin-block-start: 2rem;
          flex-wrap: wrap;
        }

        .stat {
          display: flex;
          flex-direction: column;
          gap: 0.15rem;
        }

        .stat-num {
          font-family: var(--font-display);
          font-size: 2.2rem;
          color: var(--accent);
          line-height: 1;
        }

        .stat-label {
          font-size: 0.58rem;
          letter-spacing: 0.2em;
          text-transform: uppercase;
          color: var(--muted);
        }

        .h-line {
          width: 3.125rem;
          height: var(--hairline);
          background: var(--accent);
          margin-block-end: 1.2rem;
          transform-origin: left;
        }

        .cta-row {
          display: flex;
          align-items: center;
          justify-content: flex-start;
          gap: 0.75rem;
          margin-block-start: 1.75rem;
        }

        .text-card.right .cta-row {
          justify-content: flex-end;
        }

        .cta {
          display: inline-flex;
          align-items: center;
          gap: 0.6rem;
          padding: 0.6rem 1.25rem;
          border: var(--hairline) solid var(--accent);
          color: var(--accent);
          font-family: var(--font-mono);
          font-size: 0.62rem;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          text-decoration: none;
          cursor: pointer;
          transition:
            background 0.2s,
            color 0.2s;

          &:hover {
            background: var(--accent);
            color: var(--bg);
          }

          svg {
            width: 0.6875rem;
            height: 0.6875rem;
          }
        }

        .cta-back {
          display: inline-flex;
          align-items: center;
          gap: 0.6rem;
          padding: 0.6rem 1.25rem;
          border: var(--hairline) solid
            color-mix(in srgb, var(--muted) 45%, transparent);
          color: var(--muted);
          font-family: var(--font-mono);
          font-size: 0.62rem;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          text-decoration: none;
          transition:
            background 0.2s,
            color 0.2s,
            border-color 0.2s;

          &:hover {
            background: color-mix(in srgb, var(--muted) 12%, transparent);
            border-color: var(--muted);
            color: var(--fg);
          }

          svg {
            width: 0.6875rem;
            height: 0.6875rem;
          }
        }
      }

      @layer reveal {
        :is(.tag, h1, h2, .body-text, .stat-row, .cta, .cta-back) {
          opacity: 0;
          translate: 0 var(--reveal-offset);
        }

        :is(h1, h2) {
          translate: 0 1.125rem;
          transition:
            opacity var(--reveal-duration) ease 0.08s,
            translate var(--reveal-duration) ease 0.08s;
        }

        .tag {
          transition:
            opacity var(--reveal-duration) ease,
            translate var(--reveal-duration) ease;
        }

        .body-text {
          transition:
            opacity var(--reveal-duration) ease 0.2s,
            translate var(--reveal-duration) ease 0.2s;
        }

        .stat-row {
          transition:
            opacity var(--reveal-duration) ease 0.3s,
            translate var(--reveal-duration) ease 0.3s;
        }

        :is(.cta, .cta-back) {
          transition:
            opacity var(--reveal-duration) ease 0.35s,
            translate var(--reveal-duration) ease 0.35s,
            background 0.2s,
            color 0.2s,
            border-color 0.2s;
        }

        .h-line {
          opacity: 0;
          scale: 0 1;
          transition:
            opacity 0.4s ease,
            scale 0.4s ease;
        }

        :is(.tag, h1, h2, .body-text, .stat-row, .cta, .cta-back).visible {
          opacity: 1;
          translate: 0 0;
        }

        .h-line.visible {
          opacity: 1;
          scale: 1 1;
        }
      }

      @layer theme {
        :root[data-theme="light"] {
          color-scheme: light;
          --bg: var(--light-bg);
          --fg: var(--light-fg);
          --muted: var(--light-muted);
          --accent: var(--accent-light);
          --card-bg: rgba(240, 236, 227, 0.08);
          --card-border: rgba(58, 110, 0, 0.14);

          .face {
            background:
              repeating-linear-gradient(
                0deg,
                rgba(0, 0, 0, 0.05) 0,
                rgba(0, 0, 0, 0.05) 1px,
                transparent 1px,
                transparent 48px
              ),
              repeating-linear-gradient(
                90deg,
                rgba(0, 0, 0, 0.05) 0,
                rgba(0, 0, 0, 0.05) 1px,
                transparent 1px,
                transparent 48px
              ),
              #ddd8cf;
          }

          .face-ph {
            color: rgba(0, 0, 0, 0.07);
          }

          #theme_toggle {
            svg {
              color: var(--fg);
            }
            .icon-sun {
              opacity: 0;
              rotate: -90deg;
            }
            .icon-moon {
              opacity: 1;
              rotate: 0deg;
            }
          }

          #face_caption_name {
            opacity: 0.35;
          }
        }
      }

      @layer responsive {
        @media (width <= 56.25em) {
          #hud {
            top: 1rem;
            right: 1rem;
          }

          #scene_strip {
            display: none;
          }

          #theme_toggle {
            bottom: 1rem;
            left: 1.25rem;
            translate: 0 0;
          }

          #face_caption {
            bottom: 1rem;
          }

          section {
            min-height: 150vh;
            align-items: flex-end;
            padding: 0 1.5rem 3.5rem;
          }

          #s0 {
            min-height: 100vh;
            align-items: center;
            padding: 4rem 1.5rem;
          }

          :is(.text-card, .text-card.right, .text-card.center) {
            max-width: 100%;
            padding: 1.5rem 1.25rem;
          }

          .body-text {
            line-height: 1.55;
          }

          .stat-row {
            gap: 1.5rem;
            margin-block-start: 1.25rem;
          }

          .cta-row {
            margin-block-start: 1.25rem;
          }
        }
      }
    </style>
  </head>

  <body>
    <div id="scene">
      <div id="cube">
        <div class="face" data-face="top" data-i="0">
          <span class="face-ph">TOP</span>
        </div>
        <div class="face" data-face="front" data-i="1">
          <span class="face-ph">FRONT</span>
        </div>
        <div class="face" data-face="right" data-i="2">
          <span class="face-ph">RIGHT</span>
        </div>
        <div class="face" data-face="back" data-i="3">
          <span class="face-ph">BACK</span>
        </div>
        <div class="face" data-face="left" data-i="4">
          <span class="face-ph">LEFT</span>
        </div>
        <div class="face" data-face="bottom" data-i="5">
          <span class="face-ph">BOTTOM</span>
        </div>
      </div>
    </div>

    <div id="hud">
      <div id="hud_pct">000%</div>
      <div class="progress-bar">
        <div class="progress-fill" id="prog_fill"></div>
      </div>
      <div class="scene-label" id="scene_name">DESCENT</div>
    </div>

    <button id="theme_toggle" aria-label="Toggle light/dark mode">
      <svg
        class="icon-sun"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="1.5"
      >
        <circle cx="12" cy="12" r="4" />
        <path
          d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"
        />
      </svg>
      <svg
        class="icon-moon"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="1.5"
      >
        <path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z" />
      </svg>
    </button>

    <div id="scene_strip">
      <a href="#s0" class="scene-dot active"></a>
      <a href="#s1" class="scene-dot"></a>
      <a href="#s2" class="scene-dot"></a>
      <a href="#s3" class="scene-dot"></a>
      <a href="#s4" class="scene-dot"></a>
      <a href="#s5" class="scene-dot"></a>
    </div>

    <div id="face_caption">
      <div id="face_caption_num">01</div>
      <div id="face_caption_name">DESCENT</div>
    </div>

    <div id="scroll_container">
      <section id="s0">
        <div class="text-card">
          <div class="tag">Cube Gallery — Bad Art</div>
          <h1>WORK<br />AGAINST<br />THE MODEL</h1>
          <p class="body-text">
            What happens when you ask AI to do the opposite of what it was built
            for? Break proportion. Flip symmetry. Leave the mistakes in place.
            Scroll to find out.
          </p>
          <div class="cta-row">
            <a class="cta" href="#s1">
              Enter
              <svg
                viewBox="0 0 12 12"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
              >
                <path d="M1 6h10M6 1l5 5-5 5" />
              </svg>
            </a>
          </div>
        </div>
      </section>

      <section id="s1">
        <div class="text-card right">
          <div class="h-line"></div>
          <div class="tag">01 — Art Rebellion</div>
          <h2>FLIP<br />THE<br />PROMPT</h2>
          <p class="body-text">
            A cow walking a monster instead of a monster walking a cow. That
            inversion is enough to break template thinking. The cape ends up on
            the wrong body.
          </p>
          <div class="cta-row">
            <a class="cta-back" href="#s0">
              <svg
                viewBox="0 0 12 12"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
              >
                <path d="M11 6H1M6 11L1 6l5-5" />
              </svg>
              Back
            </a>
            <a class="cta" href="#s2">
              Turn
              <svg
                viewBox="0 0 12 12"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
              >
                <path d="M1 6h10M6 1l5 5-5 5" />
              </svg>
            </a>
          </div>
        </div>
      </section>

      <section id="s2">
        <div class="text-card">
          <div class="h-line"></div>
          <div class="tag">02 — Moo Walk</div>
          <h2>NEITHER<br />LEADS</h2>
          <p class="body-text">
            Clashing colors. No balance. A dance with no choreography. When the
            model works against itself something more genuine surfaces.
          </p>
          <div class="cta-row">
            <a class="cta-back" href="#s1">
              <svg
                viewBox="0 0 12 12"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
              >
                <path d="M11 6H1M6 11L1 6l5-5" />
              </svg>
              Back
            </a>
            <a class="cta" href="#s3">
              Turn
              <svg
                viewBox="0 0 12 12"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
              >
                <path d="M1 6h10M6 1l5 5-5 5" />
              </svg>
            </a>
          </div>
        </div>
      </section>

      <section id="s3">
        <div class="text-card right">
          <div class="h-line"></div>
          <div class="tag">03 — Bad Art</div>
          <h2>REVERSE<br />CREATIVITY</h2>
          <p class="body-text">
            AI is trained to polish and regularize. The harder direction is
            unlearning that. A television for a head is not an error. It is the
            point.
          </p>
          <div class="stat-row" style="justify-content: flex-end">
            <div class="stat">
              <span class="stat-num">6</span>
              <span class="stat-label">Works</span>
            </div>
            <div class="stat">
              <span class="stat-num">360</span>
              <span class="stat-label">Degrees</span>
            </div>
            <div class="stat">
              <span class="stat-num">1</span>
              <span class="stat-label">Object</span>
            </div>
          </div>
          <div class="cta-row">
            <a class="cta-back" href="#s2">
              <svg
                viewBox="0 0 12 12"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
              >
                <path d="M11 6H1M6 11L1 6l5-5" />
              </svg>
              Back
            </a>
            <a class="cta" href="#s4">
              Turn
              <svg
                viewBox="0 0 12 12"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
              >
                <path d="M1 6h10M6 1l5 5-5 5" />
              </svg>
            </a>
          </div>
        </div>
      </section>

      <section id="s4">
        <div class="text-card">
          <div class="h-line"></div>
          <div class="tag">04 — No Rules</div>
          <h2>NONSENSE<br />AT THE<br />CENTER</h2>
          <p class="body-text">
            Dada and the surrealists knew this. Put the absurd at the center and
            the edges stop pretending. Nine heads in the branches. The sun has a
            face and it approves.
          </p>
          <div class="cta-row">
            <a class="cta-back" href="#s3">
              <svg
                viewBox="0 0 12 12"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
              >
                <path d="M11 6H1M6 11L1 6l5-5" />
              </svg>
              Back
            </a>
            <a class="cta" href="#s5">
              Turn
              <svg
                viewBox="0 0 12 12"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
              >
                <path d="M1 6h10M6 1l5 5-5 5" />
              </svg>
            </a>
          </div>
        </div>
      </section>

      <section id="s5">
        <div class="text-card right">
          <div class="h-line"></div>
          <div class="tag">05 — Super Monsters</div>
          <h2>RAW<br />NOT<br />POLISHED</h2>
          <p class="body-text">
            Forward creativity takes a sketch and makes it real. This goes the
            other way. Imperfection left in place is closer to something honest.
          </p>
          <div class="cta-row">
            <a class="cta-back" href="#s4">
              <svg
                viewBox="0 0 12 12"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
              >
                <path d="M11 6H1M6 11L1 6l5-5" />
              </svg>
              Back
            </a>
            <a class="cta" href="#s0">
              Begin again
              <svg
                viewBox="0 0 12 12"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
              >
                <path d="M1 6h10M6 1l5 5-5 5" />
              </svg>
            </a>
          </div>
        </div>
      </section>
    </div>

    <script>
      const IMAGE_SRCS = [
        "https://assets.codepen.io/573855/demo-raw-01.webp",
        "https://assets.codepen.io/573855/demo-raw-02.webp",
        "https://assets.codepen.io/573855/demo-raw-03.webp",
        "https://assets.codepen.io/573855/demo-raw-04.webp",
        "https://assets.codepen.io/573855/demo-raw-05.webp",
        "https://assets.codepen.io/573855/demo-raw-06.webp",
      ];

      const IMAGE_ASPECTS = [1, 1, 1, 1, 1, 1];

      const FACE_NAMES = [
        "DESCENT",
        "REBELLION",
        "MOO WALK",
        "BAD ART",
        "NO RULES",
        "SUPER",
      ];

      const SWAP_RADIUS = 3;

      const N = IMAGE_SRCS.length;
      const STOPS = buildStops(N);

      const stopIndex = (s) => Math.min(N - 1, Math.floor(s * (N - 1)));

      function faceAtStop(i) {
        if (i < 6) return i;
        return 1 + ((i - 2) % 4);
      }

      function buildStops(n) {
        const base = [
          { rx: 90, ry: 0 },
          { rx: 0, ry: 0 },
          { rx: 0, ry: -90 },
          { rx: 0, ry: -180 },
          { rx: 0, ry: -270 },
          { rx: -90, ry: -360 },
        ];
        const out = base.slice(0, Math.min(n, 6));
        for (let i = 6; i < n; i++) {
          out.push({ rx: 0, ry: -360 - (i - 6) * 90 });
        }
        return out;
      }

      const dom = {
        cube: document.getElementById("cube"),
        faces: [...document.querySelectorAll(".face")],
        scrollEl: document.getElementById("scroll_container"),
        strip: document.getElementById("scene_strip"),
        hudPct: document.getElementById("hud_pct"),
        progFill: document.getElementById("prog_fill"),
        sceneName: document.getElementById("scene_name"),
        captionNum: document.getElementById("face_caption_num"),
        captionName: document.getElementById("face_caption_name"),
        themeToggle: document.getElementById("theme_toggle"),
      };

      for (
        let i = dom.scrollEl.querySelectorAll("section").length;
        i < N;
        i++
      ) {
        const sec = document.createElement("section");
        sec.id = `s${i}`;
        dom.scrollEl.appendChild(sec);
      }

      dom.strip.innerHTML = "";
      for (let i = 0; i < N; i++) {
        const a = document.createElement("a");
        a.href = `#s${i}`;
        a.className = "scene-dot" + (i === 0 ? " active" : "");
        dom.strip.appendChild(a);
      }

      const sceneDots = [...document.querySelectorAll(".scene-dot")];
      const sections = [
        ...document.querySelectorAll("#scroll_container section"),
      ];

      const faceImgIdx = new Array(6).fill(-1);
      let currentStop = -1;

      const imagePromises = new Map();

      const isDark = () =>
        document.documentElement.getAttribute("data-theme") === "dark";

      const getDarkSrc = (src) => src.replace(/\.webp$/, "-dark.webp");

      const getActiveSrc = (imgIdx) => {
        const src = IMAGE_SRCS[imgIdx];
        return isDark() ? getDarkSrc(src) : src;
      };

      const preloadImage = (src) => {
        if (imagePromises.has(src)) return imagePromises.get(src);
        const p = (async () => {
          const img = new Image();
          img.src = src;
          await img.decode().catch(() => {});
          return img;
        })();
        imagePromises.set(src, p);
        return p;
      };

      IMAGE_SRCS.forEach((src) => {
        preloadImage(src);
        preloadImage(getDarkSrc(src));
      });

      async function setFaceImage(faceIdx, imgIdx, force = false) {
        if (!force && faceIdx === faceAtStop(currentStop)) return;
        if (!force && faceImgIdx[faceIdx] === imgIdx) return;
        faceImgIdx[faceIdx] = imgIdx;

        const src = getActiveSrc(imgIdx);
        const face = dom.faces[faceIdx];

        await preloadImage(src);

        if (faceImgIdx[faceIdx] !== imgIdx) return;

        let img = face.querySelector("img");
        if (!img) {
          img = new Image();
          face.appendChild(img);
        }
        img.alt = FACE_NAMES[imgIdx] ?? "";
        img.src = src;
        img.style.objectFit =
          (IMAGE_ASPECTS[imgIdx] ?? 1) !== 1 ? "contain" : "";
      }

      const refreshFaceImages = () => {
        const snapshot = [...faceImgIdx];
        faceImgIdx.fill(-1);
        snapshot.forEach((imgIdx, faceIdx) => {
          if (imgIdx !== -1) setFaceImage(faceIdx, imgIdx, true);
        });
      };

      for (let i = 0; i < Math.min(N, 6); i++) {
        if (IMAGE_SRCS[i]) setFaceImage(i, i, true);
      }

      function checkImageSwaps(smooth) {
        const base = stopIndex(smooth);
        for (let offset = -SWAP_RADIUS; offset <= SWAP_RADIUS; offset++) {
          if (offset === 0) continue;
          const si = base + offset;
          if (si < 0 || si >= N) continue;
          setFaceImage(faceAtStop(si), si);
        }
      }

      let lastFaceIdx = -1;

      const updateHUD = (s) => {
        const p = Math.round(s * 100);
        const si = sectionIndexFromScroll(scrollY);
        currentStop = si;
        dom.hudPct.textContent = String(p).padStart(3, "0") + "%";
        dom.progFill.style.width = `${p}%`;
        if (si !== lastFaceIdx) {
          lastFaceIdx = si;
          const name = FACE_NAMES[si] ?? "";
          dom.sceneName.textContent = name;
          dom.captionNum.textContent = String(si + 1).padStart(2, "0");
          dom.captionName.textContent = name;
          sceneDots.forEach((d, i) => d.classList.toggle("active", i === si));
        }
      };

      const easeIO = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);

      const setCubeTransform = (s) => {
        if (N < 2 || STOPS.length < 2) return;
        const t = s * (N - 1);
        const i = Math.min(Math.floor(t), N - 2);
        const f = easeIO(t - i);
        const a = STOPS[i];
        const b = STOPS[i + 1];
        const rx = a.rx + (b.rx - a.rx) * f;
        const ry = a.ry + (b.ry - a.ry) * f;
        dom.cube.style.transform = `rotateX(${rx}deg) rotateY(${ry}deg)`;
      };

      let sectionTops = [];

      const buildSectionTops = () => {
        sectionTops = sections.map(
          (s) => s.getBoundingClientRect().top + window.scrollY,
        );
      };

      const sectionIndexFromScroll = (y) => {
        const mid = y + innerHeight * 0.5;
        let idx = 0;
        for (let i = 0; i < sectionTops.length; i++) {
          if (mid >= sectionTops[i]) idx = i;
        }
        return Math.min(idx, N - 1);
      };

      const mq = window.matchMedia("(prefers-color-scheme: dark)");
      const getSystemTheme = () => (mq.matches ? "dark" : "light");

      const applyTheme = (theme) => {
        document.documentElement.setAttribute("data-theme", theme);
        document.documentElement.style.colorScheme = theme;
        refreshFaceImages();
      };

      applyTheme(getSystemTheme());
      mq.addEventListener("change", (e) =>
        applyTheme(e.matches ? "dark" : "light"),
      );

      dom.themeToggle.addEventListener("click", () => {
        const cur =
          document.documentElement.getAttribute("data-theme") ||
          getSystemTheme();
        applyTheme(cur === "dark" ? "light" : "dark");
      });

      const mqSmall = window.matchMedia("(max-width: 56.25em)");

      let maxScroll = 1;
      let lastScrollHeight = 0;
      let lastInnerHeight = 0;

      const resize = () => {
        const h = document.documentElement.scrollHeight;
        const vh = innerHeight;
        if (h === lastScrollHeight && vh === lastInnerHeight) return;
        lastScrollHeight = h;
        lastInnerHeight = vh;
        maxScroll = Math.max(1, h - vh);
        buildSectionTops();
      };

      resize();

      let tgt = 0;
      let smooth = 0;
      let velocity = 0;

      const ease = 0.1;
      const dynamicFriction = (v) => (Math.abs(v) > 200 ? 0.8 : 0.9);

      window.addEventListener("resize", () => {
        resize();
        tgt = maxScroll > 0 ? scrollY / maxScroll : 0;
        smooth = tgt;
      });

      let resizePending = false;
      const ro = new ResizeObserver(() => {
        if (resizePending) return;
        resizePending = true;
        requestAnimationFrame(() => {
          resize();
          tgt = maxScroll > 0 ? scrollY / maxScroll : 0;
          smooth = tgt;
          resizePending = false;
        });
      });
      ro.observe(document.documentElement);

      window.addEventListener(
        "scroll",
        () => {
          tgt = maxScroll > 0 ? scrollY / maxScroll : 0;
          tgt = Math.max(0, Math.min(1, tgt));
        },
        { passive: true },
      );

      window.addEventListener(
        "wheel",
        (e) => {
          e.preventDefault();
          const linePx = 16;
          const pagePx = innerHeight * 0.9;
          const delta =
            e.deltaMode === 1
              ? e.deltaY * linePx
              : e.deltaMode === 2
                ? e.deltaY * pagePx
                : e.deltaY;
          if (Math.abs(delta) < 5) return;
          stopAnchorAnim();
          velocity += delta;
          velocity = Math.max(-600, Math.min(600, velocity));
        },
        { passive: false },
      );

      const revealEls = [
        ...document.querySelectorAll(
          ".tag, h1, h2, .body-text, .stat-row, .cta, .cta-back, .h-line",
        ),
      ];

      const io = new IntersectionObserver(
        (entries) =>
          entries.forEach((e) => {
            if (e.isIntersecting) {
              e.target.classList.add("visible");
              io.unobserve(e.target);
            }
          }),
        { threshold: 0.1 },
      );
      revealEls.forEach((el) => io.observe(el));

      let lastNow = performance.now();

      const frame = (now) => {
        requestAnimationFrame(frame);

        if (document.hidden) {
          lastNow = now;
          return;
        }

        const dt = Math.min((now - lastNow) / 1000, 0.05);
        lastNow = now;

        velocity *= Math.pow(dynamicFriction(velocity), dt * 60);
        if (Math.abs(velocity) < 0.01) velocity = 0;

        if (Math.abs(velocity) > 0.2) {
          const next = Math.max(
            0,
            Math.min(scrollY + velocity * ease, maxScroll),
          );
          window.scrollTo(0, next);
          tgt = next / maxScroll;
        }

        smooth += (tgt - smooth) * (1 - Math.exp(-dt * 8));
        smooth = Math.max(0, Math.min(1, smooth));

        updateHUD(smooth);
        checkImageSwaps(smooth);
        setCubeTransform(smooth);
      };

      requestAnimationFrame(frame);

      let anchorAnim = null;
      let isAnchorScrolling = false;

      const stopAnchorAnim = () => {
        if (anchorAnim) {
          cancelAnimationFrame(anchorAnim);
          anchorAnim = null;
        }
        isAnchorScrolling = false;
      };

      const easeInOutCubic = (t) =>
        t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;

      const smoothScrollToY = (targetY, duration = 900) => {
        stopAnchorAnim();
        velocity = 0;
        isAnchorScrolling = true;
        const startY = window.scrollY;
        const diff = targetY - startY;
        const start = performance.now();
        const tick = (now) => {
          const p = Math.min(1, (now - start) / duration);
          const y = startY + diff * easeInOutCubic(p);
          window.scrollTo(0, y);
          tgt = y / maxScroll;
          smooth = tgt;
          if (p < 1) {
            anchorAnim = requestAnimationFrame(tick);
          } else {
            anchorAnim = null;
            isAnchorScrolling = false;
          }
        };
        anchorAnim = requestAnimationFrame(tick);
      };

      window.addEventListener("touchstart", stopAnchorAnim, { passive: true });
      window.addEventListener("mousedown", stopAnchorAnim, { passive: true });
      window.addEventListener("keydown", stopAnchorAnim);

      document.addEventListener("click", (e) => {
        const a = e.target.closest('a[href^="#s"]');
        if (!a) return;
        const target = document.querySelector(a.getAttribute("href"));
        if (!target) return;
        e.preventDefault();
        const isHero = a.getAttribute("href") === "#s0";
        const idx = sections.indexOf(target);
        const baseY =
          idx >= 0
            ? sectionTops[idx]
            : target.getBoundingClientRect().top + window.scrollY;
        const extraOffset =
          mqSmall.matches && !isHero
            ? Math.max(0, target.offsetHeight - innerHeight)
            : 0;
        smoothScrollToY(Math.max(0, baseY + extraOffset));
      });
    </script>
  </body>
</html>