@charset "UTF-8";

/* ==================================================================
 * base.css — reset / custom properties / typography foundation
 * Design base widths: PC 1280px (half of 2560px comp) / SP 393px (half of 786px comp)
 * 1rem = 10px. PC scales fluidly between 1280-1600px, fixed above that.
 * ================================================================== */

/* ---------------------------------------------------------------- Reset */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  /* overflow-x:hidden on html/body breaks touch vertical scrolling; use clip */
  overflow-x: clip;
  /* The loader and the message hold both set overflow:hidden briefly;
     keep the scrollbar gutter so the page doesn't jump sideways
     (no effect with overlay scrollbars, e.g. macOS default). */
  scrollbar-gutter: stable;
  /* Base color on html. Without it the body background propagates to the
     canvas, and the gradient maps to viewport height instead of page height. */
  background: var(--c-bg);
  /* 1rem = 10px so values can be written px-style.
     Uses % instead of a px value so it still follows the browser's
     font-size setting (default 16px). */
  font-size: 62.5%;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: auto;
}

body {
  overflow-x: clip;
  touch-action: manipulation;
  /* Matches the comp's background sampled top to bottom:
     slow cool -> warm -> cool shift across the whole page.
       to 28% (before MANIFEST)   cool #edf1f4
       45% (around FUN CLUB)      warm #f3eee7 at its strongest
       76% down (below LIVE)      back to cool
     Painting per section would show seams, so one gradient on body. */
  background: #eef0f3;
  color: var(--c-text);
  font-family: var(--font-mincho);
  font-size: 1.5rem;
  line-height: 2;
  font-feature-settings: "palt";
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img,
svg,
canvas,
video {
  display: block;
  max-width: 100%;
}

img {
  height: auto;
}

a {
  color: inherit;
  text-decoration: none;
}

button {
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

ul,
ol {
  list-style: none;
}

h1, h2, h3, h4, p, figure {
  font-size: inherit;
  font-weight: inherit;
}

/* ---------------------------------------------------------------- Tokens */
:root {
  /* color */
  --c-bg: #edf1f4;
  --c-bg-warm: #f3eee7;
  --c-text: #1b1b1b;
  --c-text-thin: #4a4a4a;

  /* FAN CLUB paper: flat single color per artboard 1, no torn edge
     (sampled #e3bd8c-#e9c393, midpoint used) */
  --c-paper: #e5bf8e;
  --c-paper-ink: #6d5531;

  /* font */
  --font-mincho: "ten-mincho", "貂明朝", "Yu Mincho", "YuMincho", "Hiragino Mincho ProN", "Noto Serif JP", serif;
  --font-serif: "Source Serif 4", "Source Serif Variable", "Source Serif Pro", "ten-mincho", Georgia, serif;

  /* motion */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
  /* Slow spread ease; --ease-out rushes its first half, so not used here */
  --ease-spread: cubic-bezier(0.35, 0.05, 0.25, 1);

  /* layer */
  --z-kv: 1;
  --z-menu: 90;
  --z-header: 95;
  --z-loader: 100;
}

/* Base is 62.5% above (1rem = 10px), made fluid against the comp widths
 * (PC 1280px / SP 393px): PC 1280px -> 1rem = 10px, SP 393px -> 1rem = 10px.
 * Raw calc() alone jumps to 19.5px at 768px and 6px at 769px, so clamp()
 * caps both ends. Bounds are in em so they track browser settings like 62.5%
 * (0.625em = 10px base; 0.5625em = 9px, 0.78125em = 12.5px).
 * ------------------------------------------------------------------------- */

/* SP: 393px base. 11px cap so text doesn't balloon on large phones */
@media (max-width: 768px) {

  body {
    font-size: 1.4rem;
  }
}

/* PC/tablet: 1280px base. 9.5px floor so narrow viewports don't crush,
   12.5px cap (= 1600px equivalent) so wide screens don't overgrow */

/* ---------------------------------------------------------------- Lenis */
html.lenis,
html.lenis body {
  height: auto;
}

.lenis.lenis-smooth {
  scroll-behavior: auto !important;
}

.lenis.lenis-smooth [data-lenis-prevent] {
  overscroll-behavior: contain;
}

.lenis.lenis-stopped {
  overflow: hidden;
}

/* ---------------------------------------------------------------- Selection / focus */
::selection {
  background: rgba(211, 186, 150, 0.55); /* paper-colored selection */
  color: #1b1b1b;
}

:focus-visible {
  outline: 1px dashed currentColor;
  outline-offset: 4px;
}

/* ---------------------------------------------------------------- Utility */
/* PC/SP visibility switch.
   The "hide" side is !important: at equal specificity with classes that set
   display (e.g. .funclub__line), load order could make both render
   (actually happened — an SP-only line showed on PC and stretched the paper 80px). */
.u-pc {
  display: block;
}

.u-sp {
  display: none !important;
}

@media (max-width: 768px) {
  .u-pc {
    display: none !important;
  }

  .u-sp {
    display: block !important;
  }

  /* <br class="u-sp"> must stay inline; block changes line-break behavior */
  br.u-sp {
    display: inline !important;
  }
}

/* Visually hidden but kept for screen readers and search */
.u-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  border: 0;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* SVG holding only filter defs; not rendered, but display:none would break references */
.u-svg-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
  pointer-events: none;
}

/* ---------------------------------------------------------------- Boil
 * Cycles 3 noise-distorted frames so handwritten strokes appear to wobble.
 * - .is-boiling is added only while in viewport (main.js)
 * - --boil-pre can hold a base filter to compose with (e.g. KV knockout)
 * Wobble amount / grain size are tuned in the filter defs in index.html.
 * -------------------------------------------------------------------------- */
[data-boil] {
  filter: var(--boil-pre, ) url("#boil-1");
}

[data-boil].is-boiling {
  animation: boil 0.36s steps(1, end) infinite;
}

/* Touch devices: main.js sets .is-scroll-hold on body while scrolling;
   pausing the swap there keeps filter repaints out of the scroll frames
   (covers the hard variant below too). */
body.is-scroll-hold [data-boil].is-boiling {
  animation-play-state: paused;
}

/* Handwriting-animated headings start boiling only after the stroke is done;
   distorting mid-draw makes the stroke reveal itself illegibly.
   .is-written is added by main.js when drawing finishes. */
[data-boil][data-handwrite]:not(.is-written),
[data-boil][data-handwrite]:not(.is-written).is-boiling {
  filter: var(--boil-pre, none);
  animation: none;
}

@keyframes boil {
  0%   { filter: var(--boil-pre, ) url("#boil-1"); }
  33%  { filter: var(--boil-pre, ) url("#boil-2"); }
  66%  { filter: var(--boil-pre, ) url("#boil-3"); }
}

/* data-boil="hard": for blacked-out (pre-announcement) blocks.
   Same 3 frames / 0.36s as titles, only the wobble amplitude is larger. */
[data-boil="hard"] {
  filter: url("#boil-h1");
}

[data-boil="hard"].is-boiling {
  animation: boilHard 0.36s steps(1, end) infinite;
}

@keyframes boilHard {
  0%   { filter: url("#boil-h1"); }
  33%  { filter: url("#boil-h2"); }
  66%  { filter: url("#boil-h3"); }
}

/* Once JS drives the noise (all boils on touch — iOS never repaints the
   CSS frame swap), stop the CSS frame cycling (if JS fails, the frame
   cycling above remains as a fallback) */
[data-boil].is-js-boil,
[data-boil].is-js-boil.is-boiling {
  animation: none;
}

/* No filters at all on touch devices (avoids scroll jank) */
@media (hover: none) {
  [data-boil],
  [data-boil].is-boiling,
  [data-boil="hard"],
  [data-boil="hard"].is-boiling {
    filter: var(--boil-pre, none);
    animation: none;
  }
}

/* Reduced motion: stop the distortion entirely */
@media (prefers-reduced-motion: reduce) {
  [data-boil],
  [data-boil].is-boiling,
  [data-boil="hard"],
  [data-boil="hard"].is-boiling {
    filter: var(--boil-pre, none);
    animation: none;
  }
}

/* When the WebGL reveal is active, skip the CSS variants (mask / negative).
   Target photos stay hidden to avoid a flash before the effect starts. */
.has-gl-reveal [data-reveal="mask"] {
  opacity: 1;
  -webkit-mask-image: none;
  mask-image: none;
  transition: none;
}

.has-gl-reveal [data-reveal="mask"] > * {
  transform: none;
  filter: none;
  transition: none;
  animation: none;
}

/* Don't hide the photo. The canvas only overlays an "undeveloped negative"
   film on top, so no blank gap is visible before or after the effect. */
canvas.gl-reveal {
  image-rendering: auto;
}

/* Photos waiting for their entrance; JS (photo-in.js) animates them in.
   Hide only when .has-gl-reveal is present, so photos never stay hidden
   if the JS effect is unavailable (no JS / reduced motion). */
.has-gl-reveal [data-gl-reveal] {
  opacity: 0;
}

/* Same, for the photo that enters under the negative film instead
   (image-reveal.js; .has-film-reveal is set only once it has a WebGL
   context). It is faded in with the film already applied, so it must not be
   on screen in full colour beforehand. */
.has-film-reveal [data-film-reveal] {
  opacity: 0;
}

/* data-reveal="chars": fade body text in one character at a time.
   Blur, upward offset, and opacity resolve together, each character
   delayed by 0.032s. --i is assigned per character by main.js. */
[data-reveal="chars"] {
  opacity: 1;
  transform: none;
  transition: none;
}

.chars__word {
  display: inline-block;
  white-space: nowrap;
}

/* --l lives on the line (p), but custom properties inherit so characters can read it */

/* Initial hidden state is set by main.js (initStage) via gsap.set.
   Not hidden here with opacity:0 — if the timeline ever failed,
   the text would never appear.

   NOTE: never put filter or will-change on individual characters.
   That creates ~157 layers and Chrome gives up compositing,
   dropping the text entirely. Blur is applied once on the line (p). */
.chars__c {
  display: inline-block;
}

@media (prefers-reduced-motion: reduce) {
  .chars__c,
  [data-reveal="chars"] .message__sign {
    opacity: 1;
    transform: none;
    filter: none;
    transition: none;
  }
}

/* Scroll reveal. Once shown, stays shown (no replay on scroll back) */
[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 3rem, 0);
  transition:
    opacity 1.2s var(--ease-out),
    transform 1.2s var(--ease-out);
  will-change: opacity, transform;
}

[data-reveal].is-shown {
  opacity: 1;
  transform: none;
}

body.is-loading [data-reveal] {
  transition: none;
}

/* data-reveal="mask": photo reveal
 *   From transparent, a blurred mask spreads softly outward while the image
 *   develops from a negative (inverted + desaturated + blurred) into color.
 *   Interpolating invert linearly passes through flat gray at invert(0.5),
 *   so the keyframes are weighted to snap through that region quickly.
 * -------------------------------------------------------------------------- */
[data-reveal="mask"] {
  transform: none;
  opacity: 0;
  overflow: clip;
  -webkit-mask-image: radial-gradient(circle, #000 26%, rgba(0, 0, 0, 0) 74%);
  mask-image: radial-gradient(circle, #000 26%, rgba(0, 0, 0, 0) 74%);
  -webkit-mask-position: center 64%;
  mask-position: center 64%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 0% 0%;
  mask-size: 0% 0%;
  transition: none;
  will-change: mask-size, opacity;
}

body:not(.is-loading) [data-reveal="mask"] {
  transition:
    opacity 1.6s var(--ease-spread),
    -webkit-mask-size 2.4s var(--ease-spread),
    mask-size 2.4s var(--ease-spread);
}

[data-reveal="mask"].is-shown {
  opacity: 1;
  -webkit-mask-size: 320% 320%;
  mask-size: 320% 320%;
}

[data-reveal="mask"] > * {
  transform: scale(1.12);
  filter: invert(1) saturate(0.25) contrast(1.1) blur(1.4rem);
  transition: transform 2.4s var(--ease-spread);
  will-change: transform, filter;
}

[data-reveal="mask"].is-shown > * {
  transform: none;
  filter: none;
  animation: develop 2.3s var(--ease-spread) forwards;
}

@keyframes develop {
  0%   { filter: invert(1)    saturate(0.25) contrast(1.1)  blur(1.4rem); }
  45%  { filter: invert(0.86) saturate(0.4)  contrast(1.08) blur(0.7rem); }
  70%  { filter: invert(0.18) saturate(0.8)  contrast(1.02) blur(0.2rem); }
  100% { filter: none; }
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal="mask"] {
    opacity: 1;
    -webkit-mask-image: none;
    mask-image: none;
  }

  [data-reveal="mask"] > *,
  [data-reveal="mask"].is-shown > * {
    transform: none;
    filter: none;
    transition: none;
    animation: none;
  }
}

/* Scroll reveal. Once shown, stays shown (no replay on scroll back) */
[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 3rem, 0);
  transition:
    opacity 1.2s var(--ease-out),
    transform 1.2s var(--ease-out);
  will-change: opacity, transform;
}

[data-reveal].is-shown {
  opacity: 1;
  transform: none;
}

body.is-loading [data-reveal] {
  transition: none;
}

/* data-reveal="mask": photos reveal via a mask opening from the bottom
   instead of a fade. The photo also eases from slightly enlarged to 1:1,
   giving a peeled-open feel. */
[data-reveal="mask"] {
  opacity: 1;
  transform: none;
  transition: none;
  overflow: clip;
  clip-path: inset(100% 0 0 0);
  will-change: clip-path;
}

/* Color develops from a negative (inverted). Interpolating invert linearly
   passes through flat gray at invert(0.5), so that region snaps through fast. */
[data-reveal="mask"] > * {
  transform: scale(1.12);
  filter: invert(1) saturate(0.25) contrast(1.15);
  transition: transform 1.8s var(--ease-out);
  will-change: transform, filter;
}

[data-reveal="mask"].is-shown > * {
  transform: none;
  filter: none;
  animation: develop 1.7s var(--ease-out) forwards;
}

@keyframes develop {
  0%   { filter: invert(1)    saturate(0.25) contrast(1.15); }
  50%  { filter: invert(0.86) saturate(0.35) contrast(1.12); }
  68%  { filter: invert(0.2)  saturate(0.75) contrast(1.04); }
  100% { filter: none; }
}

body:not(.is-loading) [data-reveal="mask"] {
  transition: clip-path 1.5s var(--ease-out);
}

[data-reveal="mask"].is-shown {
  clip-path: inset(0 0 0 0);
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal="mask"] {
    clip-path: none;
  }

  [data-reveal="mask"] > *,
  [data-reveal="mask"].is-shown > * {
    transform: none;
    filter: none;
    transition: none;
    animation: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }
}


/* ==================================================================
   Loader
   Lives here, not in style.css, on purpose: base.css is the small file
   that gates the first paint, so the loader is fully styled the moment
   anything can be drawn. style.css loads without blocking rendering, and
   everything it styles is covered by this loader until it arrives.
   Only base.css variables are used below - keep it that way.
   ================================================================== */

/* ================================================================ Loader */
/* Opens with the same hazy mask as the photos. The hole radius
   (--loader-hole) and the width of its feathered edge (--loader-feather)
   are registered with @property so they can animate.
   Both stay >= 0 on purpose: WebKit clamps a negative colour-stop position
   to 0, which collapsed the gradient and made the whole mask transparent -
   Safari then painted no loader at all (only the body.is-loading backing
   layer showed) until the open animation lifted the value back above 0. */
@property --loader-hole {
  syntax: "<percentage>";
  initial-value: 0%;
  inherits: false;
}

@property --loader-feather {
  syntax: "<percentage>";
  initial-value: 0.01%;
  inherits: false;
}

.loader {
  position: fixed;
  /* No gutter correction here any more: the scrollbar stays on screen for the
     whole load (see html.has-sb below), so the gutter html reserves holds the
     real bar and this overlay's centre is already the centre of the page. */
  /* Past both edges, not inset:0: on iOS reloads the toolbar geometry can
     briefly leave a band above/below the fixed viewport uncovered, letting
     the header and KV catch peek out at the top. Symmetric overhang keeps
     the counter (grid-centered) and the mask center visually unchanged. */
  inset: -12vh 0;
  z-index: var(--z-loader);
  display: grid;
  place-items: center;
  background: var(--c-bg);
  /* Scroll gestures start on this fixed overlay; cut them so the page
     can't be scrolled mid-load (iOS ignores overflow:hidden on body). */
  touch-action: none;
  overscroll-behavior: contain;
  /* At rest the hole has no radius and no feather, so the mask is opaque
     everywhere and the KV cannot show through the centre while loading.
     The feather widens to 30% as the hole opens, giving the same hazy edge
     the photos use. */
  --loader-hole: 0%;
  --loader-feather: 0.01%;
  -webkit-mask-image: radial-gradient(circle at 50% 46%, transparent var(--loader-hole), #000 calc(var(--loader-hole) + var(--loader-feather)));
  mask-image: radial-gradient(circle at 50% 46%, transparent var(--loader-hole), #000 calc(var(--loader-hole) + var(--loader-feather)));
  /* The KV's own reveal is the star; the loader opens quickly and yields. */
  transition:
    --loader-hole 0.9s var(--ease-spread),
    --loader-feather 0.9s var(--ease-spread),
    opacity 0.45s var(--ease-out) 0.5s,
    visibility 1s;
}

.loader.is-hidden {
  --loader-hole: 100%;
  --loader-feather: 30%;
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
}

/* Re-covering on pagehide must be instant: iOS snapshots the page for the
   back-forward cache, and the snapshot has to show the loader already
   closed over the content (main.js adds this class). */
.loader.is-instant {
  transition: none;
}

/* No scrolling while the loader is up.
   html carries overflow-x:clip, so its own overflow (not body's) is what the
   viewport uses - body:overflow:hidden alone propagated nowhere and the page
   could be wheel-scrolled behind the loader. Lock the root as well.
   (Lenis turns the wheel into programmatic scrolling, which overflow does not
   block at all - main.js keeps it stopped until hideLoader().)

   Skipped on classic-scrollbar systems (html.has-sb, set by main.js): there
   the lock takes the scrollbar off screen for the whole load and hands it
   back at the open, so the page is left scrollable and main.js pins it to
   the top instead. Overlay-scrollbar systems (macOS default, iOS, Android)
   have no bar to keep, and iOS only ever obeys this lock, so they keep it. */
html.is-loading:not(.has-sb),
html:not(.has-sb) body.is-loading {
  overflow: hidden;
}

/* Lenis is stopped for the whole load as well, and its own rule further up
   (.lenis.lenis-stopped) takes the bar away just the same. Hand it back, for
   the load only: the menu also stops Lenis and still hides it.
   overflow-y alone, so html keeps the overflow-x:clip it is laid out with. */
html.has-sb.is-loading.lenis-stopped {
  overflow-x: clip;
  overflow-y: auto;
}

/* Unmasked backing layer. .loader itself carries a mask-image, and a masked
   full-screen layer can miss a frame while the mask rasterizes, letting the KV
   show through. This plain opaque layer sits just under the loader and is
   dropped in the same frame the loader starts opening. */
body.is-loading::after {
  content: "";
  position: fixed;
  inset: -12vh 0;
  z-index: calc(var(--z-loader) - 1);
  background: var(--c-bg);
}

/* Same reload flash, second layer of defense: the page chrome stays
   hidden until the loader opens (is-loading is removed right as the
   mask starts opening, so nothing pops early). */
body.is-loading .header,
body.is-loading .kv__catch,
body.is-loading .sub-hero__logo {
  visibility: hidden;
}

/* Fade the mark out before the hole opens. */
.loader.is-hidden .loader__inner {
  opacity: 0;
  transition: opacity 0.4s var(--ease-out);
}

.loader__inner {
  display: grid;
  justify-items: center;
  max-width: min(86vw, 40rem);
  transition: opacity 0.4s var(--ease-out);
}

/* Cat + 15 mark, same asset as the header button. Two stacked copies of the
   same drawing: a faint base, and a full-ink fill revealed bottom-up over a
   fixed length (main.js sets --loader-fill and adds .is-filling once). */
.loader__mark {
  position: relative;
  width: 10.8rem;
  aspect-ratio: 40.71 / 48.83;
}

.loader__mark-svg {
  display: block;
  width: 100%;
  height: 100%;
}

.loader__mark .cls-1 {
  fill-rule: evenodd;
}

.loader__mark use {
  fill: var(--c-text);
}

.loader__mark-base {
  opacity: 0.14;
}

/* The ink. Two layers with one shared transition: the clip window rises from
   below the mark while its contents sink by exactly as much, so the drawing
   never appears to move and only the filled height grows.
   Both are plain transforms, so the whole fill is handed to the compositor
   and keeps a steady rate through everything the main thread is doing behind
   the loader (image decodes, texture uploads, shader compiles) - a clip-path
   or height would be repainted per frame on that same busy thread, which is
   what read as stutter. */
.loader__mark-clip,
.loader__mark-shift {
  position: absolute;
  inset: 0;
  transition: transform var(--loader-fill, 2200ms) var(--ease-spread);
  will-change: transform;
}

.loader__mark-clip {
  overflow: hidden;
  transform: translate3d(0, 100%, 0);
}

.loader__mark-shift {
  transform: translate3d(0, -100%, 0);
}

.loader.is-filling .loader__mark-clip,
.loader.is-filling .loader__mark-shift {
  transform: translate3d(0, 0, 0);
}

@media (max-width: 768px) {
  .loader__mark {
    width: 7.3rem;
  }
}

/* Sub pages: the page stays put until the entrance has swept the viewport
   (html.is-entering, set by main.js from the open to 2.6s after) - a scroll
   mid-sweep would carry the content's cloud mask away with it. Same lock as
   the load; touch panning is cut as well for the phones that ignore it. */
html.is-entering:not(.has-sb) {
  overflow: hidden;
}

html.is-entering,
html.is-entering body {
  touch-action: none;
}
}
}
