/* ============================================================
   aozora · world.css — token layer, phase 0 starter
   ------------------------------------------------------------
   Copied verbatim from the design system's tokens/ on 2026-08-19:
     D:/DESIGN/Aozora 青空 Design System/tokens/
   Files, in the DS's own import order: fonts.css, colors.css, typography.css, spacing.css, radius.css, shadow.css, glass.css, gradients.css, motion.css, semantic.css
   Source of truth is still D:/DESIGN — re-read SKILL.md / readme /
   tokens there before building. Resets, base layouts and component
   CSS were deliberately NOT copied: layout is this world's own job.

   Google Fonts @import lines were removed (rule: fonts are self-hosted,
   see docs/04-world-contract.md §fonts). The families each token file
   expected are listed right where the import used to be.
   ============================================================ */

/* ---------- tokens/fonts.css ---------- */
/* Webfonts — loaded via Google Fonts CDN.
   Substitution notes (flagged to user):
   - Display / Latin: Inter — clean geometric sans, closest free analog to the
     Helvetica-ish poster feel.
   - Japanese: Noto Sans JP — free analog to 見出ゴ / ヒラギノ角ゴ.
   - Chinese: PingFang SC is bundled with macOS/iOS; we fall back to the system
     stack. If you ship a custom Chinese face later, drop @font-face here and it
     will reach all consumers automatically. */

/* [phase-0] Google Fonts import removed. Self-host these families
   (see docs/04-world-contract.md §fonts): Inter, Noto Sans JP, Noto Serif JP, Fraunces, Bebas Neue, Space Grotesk */

/* ---------- tokens/colors.css ---------- */
/* Aozora color scales.
   Base scales (raw values) live here. Semantic aliases live in semantic.css. */

:root {
  /* ── Sky blue — the primary scale. Tuned to feel like a Pocari can on a
     bright summer day: saturated, slightly cool, never navy-corporate. */
  --blue-50:  #EAF5FF;
  --blue-100: #CFE8FF;
  --blue-200: #9DD2FF;
  --blue-300: #66B7FF;
  --blue-400: #339BFF;
  --blue-500: #0A84FF;   /* primary */
  --blue-600: #006FE0;
  --blue-700: #005BB8;
  --blue-800: #004690;
  --blue-900: #003068;

  /* ── Sun yellow — the accent. Used sparingly: a single chip, a focus ring,
     a poster slash. Never as a background tile. */
  --sun-100: #FFF6CC;
  --sun-200: #FFEE99;
  --sun-300: #FFE980;
  --sun-400: #FFE34D;    /* accent */
  --sun-500: #FFD000;
  --sun-600: #E0B500;

  /* ── Cool neutrals — slight blue cast so whites feel like sky-lit paper,
     not screen-gray. */
  --neutral-0:   #FFFFFF;
  --neutral-50:  #F7FAFD;
  --neutral-100: #EEF3F8;
  --neutral-200: #DCE5EF;
  --neutral-300: #BCC9D6;
  --neutral-400: #8A9AAB;
  --neutral-500: #5C6B7C;
  --neutral-600: #3D4956;
  --neutral-700: #232C36;
  --neutral-800: #131922;
  --neutral-900: #07090E;

  /* ── Supporting summer hues — used in gradients, illustrations,
     occasional badge accents. Not part of the everyday UI palette. */
  --mint-400:   #5FE3CC;
  --mint-500:   #34D9C2;
  --coral-400:  #FF8A80;
  --coral-500:  #FF6B6B;
  --peach-400:  #FFB199;
  --lilac-400:  #C8B8FF;

  /* ── Semantic raw values */
  --success-500: #34C759;
  --warn-500:    #FF9F0A;
  --danger-500:  #FF453A;
  --info-500:    var(--blue-500);
}

/* ---------- tokens/typography.css ---------- */
/* Type scale — poster-grade contrast.
   Display sizes go BIG (the brand lives on the headline). Body stays calm. */

:root {
  /* ── Font families. Latin first so kerning wins on mixed strings; CJK
     fallbacks pick up Japanese and Chinese glyphs as needed. */
  --font-sans:
    "Inter",
    "PingFang SC",
    "Noto Sans JP",
    "Hiragino Sans",
    "Hiragino Kaku Gothic ProN",
    "Microsoft YaHei",
    system-ui,
    -apple-system,
    sans-serif;

  /* Display stack — same families, used at headline sizes with tight
     tracking and dense weight. */
  --font-display:
    "Inter",
    "Noto Sans JP",
    "PingFang SC",
    "Hiragino Sans",
    system-ui,
    sans-serif;

  /* Poster — for hero/marketing headlines. Tall condensed all-caps voice. */
  --font-poster:
    "Bebas Neue",
    "Inter",
    "Noto Sans JP",
    sans-serif;

  /* Digital wordmark — used in product chrome where Inter feels too neutral. */
  --font-wordmark:
    "Space Grotesk",
    "Inter",
    "Noto Sans JP",
    sans-serif;

  /* Editorial display serif — opt-in alternate for posters/activity pages
     that want a more elegant, magazine-y voice. NOT the brand default;
     reach for it only when sans feels too plain for the moment. */
  --font-display-serif:
    "Fraunces",
    "Noto Serif JP",
    "Songti SC",
    "STSong",
    serif;

  /* Mono — for numerals, timestamps, code snippets. */
  --font-mono: "JetBrains Mono", "SF Mono", ui-monospace, Menlo, monospace;

  /* ── Sizes (px). The jump from --text-2xl to --text-display-sm is
     intentional — the brand's voice is BIG headline or tiny caption,
     little in between. */
  --text-3xs:        10px;
  --text-2xs:        11px;
  --text-xs:         12px;
  --text-sm:         14px;
  --text-base:       16px;
  --text-lg:         18px;
  --text-xl:         22px;
  --text-2xl:        28px;
  --text-3xl:        36px;
  --text-display-xs: 48px;
  --text-display-sm: 64px;
  --text-display-md: 96px;
  --text-display-lg: 144px;
  --text-display-xl: 200px;   /* poster hero */

  /* ── Weights */
  --weight-light:    300;
  --weight-regular:  400;
  --weight-medium:   500;
  --weight-semibold: 600;
  --weight-bold:     700;
  --weight-black:    900;

  /* ── Line heights */
  --leading-tight:   1.05;   /* display */
  --leading-snug:    1.2;    /* headings */
  --leading-normal:  1.4;    /* body */
  --leading-relaxed: 1.6;    /* long-form */

  /* ── Tracking */
  --tracking-tighter: -0.03em;   /* huge display */
  --tracking-tight:   -0.015em;  /* headings */
  --tracking-normal:  0;
  --tracking-wide:    0.04em;    /* eyebrow / caption */
  --tracking-wider:   0.16em;    /* tiny all-caps */
}

/* ---------- tokens/spacing.css ---------- */
/* 4px base grid. The brand uses generous whitespace — when in doubt, double it. */

:root {
  --space-0:   0;
  --space-px:  1px;
  --space-0_5: 2px;
  --space-1:   4px;
  --space-2:   8px;
  --space-3:   12px;
  --space-4:   16px;
  --space-5:   20px;
  --space-6:   24px;
  --space-7:   28px;
  --space-8:   32px;
  --space-10:  40px;
  --space-12:  48px;
  --space-14:  56px;
  --space-16:  64px;
  --space-20:  80px;
  --space-24:  96px;
  --space-32:  128px;
  --space-40:  160px;
  --space-48:  192px;

  /* Semantic spacing aliases */
  --gutter-page-sm:    var(--space-4);
  --gutter-page-md:    var(--space-8);
  --gutter-page-lg:    var(--space-16);
  --gutter-card-tight: var(--space-4);
  --gutter-card:       var(--space-6);
  --gutter-card-loose: var(--space-8);
  --stack-tight:       var(--space-2);
  --stack:             var(--space-4);
  --stack-loose:       var(--space-8);
  --stack-section:     var(--space-16);
}

/* ---------- tokens/radius.css ---------- */
/* Corner radii.
   Glass-era curves: chips are pills, cards are generous (20–28px),
   page-level surfaces lean larger still. Hard 90° corners are reserved
   for poster-mode editorial blocks. */

:root {
  --radius-none: 0;
  --radius-xs:   4px;    /* tags, chips inline */
  --radius-sm:   8px;    /* inputs, small buttons */
  --radius-md:   14px;   /* default button, alert */
  --radius-lg:   20px;   /* card */
  --radius-xl:   28px;   /* glass card, modal */
  --radius-2xl:  36px;   /* hero glass panel */
  --radius-3xl:  48px;   /* full-bleed bento tile */
  --radius-pill: 999px;
  --radius-circle: 50%;
}

/* ---------- tokens/shadow.css ---------- */
/* Shadows are tinted with the brand blue so cards feel anchored to a sky-lit
   scene rather than a generic gray drop. Avoid pure black shadows. */

:root {
  /* Outer drops — soft, blue-tinted */
  --shadow-xs:   0 1px 2px  rgba(10, 68, 140, 0.06);
  --shadow-sm:   0 2px 8px  rgba(10, 68, 140, 0.08);
  --shadow-md:   0 6px 18px rgba(10, 68, 140, 0.12);
  --shadow-lg:   0 12px 36px rgba(10, 68, 140, 0.16);
  --shadow-xl:   0 24px 60px rgba(10, 68, 140, 0.22);

  /* Glow — for hover/focus accents */
  --shadow-glow-blue: 0 0 0 6px rgba(10, 132, 255, 0.18),
                      0 0 32px  rgba(10, 132, 255, 0.45); /* @kind shadow */
  --shadow-glow-sun:  0 0 0 6px rgba(255, 208, 0, 0.20),
                      0 0 28px  rgba(255, 227, 77, 0.55); /* @kind shadow */

  /* Inset highlights — give surfaces a top-lit, condensation feel */
  --inset-rim:
    inset 0 1px 0 rgba(255, 255, 255, 0.85),
    inset 0 -1px 0 rgba(10, 68, 140, 0.10); /* @kind shadow */
  --inset-rim-strong:
    inset 0 2px 0 rgba(255, 255, 255, 0.95),
    inset 0 -2px 0 rgba(10, 68, 140, 0.14); /* @kind shadow */

  /* Focus ring — keyboard accessibility, brand-tinted */
  --ring-focus: 0 0 0 3px rgba(10, 132, 255, 0.45); /* @kind shadow */
}

/* ---------- tokens/glass.css ---------- */
/* iOS 26 liquid-glass tokens.
   Three tiers of glass — thin, regular, thick — plus a colored "tinted" variant
   for tonal surfaces (e.g. a blue glass header over a yellow gradient).

   Usage pattern:
     background: var(--glass-bg-regular);
     backdrop-filter: var(--glass-blur-regular);
     box-shadow: var(--glass-edge), var(--shadow-md);
     border: 1px solid var(--glass-stroke);

   Always pair backdrop-filter with -webkit-backdrop-filter (handled below).
*/

:root {
  /* Fill — translucent whites for over-color surfaces */
  --glass-bg-thin:    rgba(255, 255, 255, 0.30);
  --glass-bg-regular: rgba(255, 255, 255, 0.48);
  --glass-bg-thick:   rgba(255, 255, 255, 0.68);

  /* Tinted glass — when you want a brand-colored frosted pane */
  --glass-bg-blue:    rgba(10, 132, 255, 0.22);
  --glass-bg-sun:     rgba(255, 227, 77, 0.28);
  --glass-bg-dark:    rgba(7, 9, 14, 0.42);

  /* Blur strength */
  --glass-blur-thin:    blur(12px) saturate(140%);  /* @kind other */
  --glass-blur-regular: blur(24px) saturate(160%);  /* @kind other */
  --glass-blur-thick:   blur(40px) saturate(180%);  /* @kind other */

  /* Stroke / rim — the bright top edge that makes glass read as glass */
  --glass-stroke:        rgba(255, 255, 255, 0.55);
  --glass-stroke-strong: rgba(255, 255, 255, 0.78);
  --glass-stroke-dark:   rgba(10, 68, 140, 0.18);

  /* Composite edge — apply as box-shadow to get the high-light rim */
  --glass-edge:
    inset 0 1px 0 rgba(255, 255, 255, 0.90),
    inset 0 0 0 1px rgba(255, 255, 255, 0.18),
    inset 0 -1px 0 rgba(10, 68, 140, 0.08); /* @kind shadow */

  --glass-edge-strong:
    inset 0 2px 0 rgba(255, 255, 255, 0.95),
    inset 0 0 0 1px rgba(255, 255, 255, 0.35),
    inset 0 -2px 0 rgba(10, 68, 140, 0.12); /* @kind shadow */
}

/* Utility classes — opt-in. Components can also just use the tokens directly. */
.glass-thin,
.glass-regular,
.glass-thick {
  border: 1px solid var(--glass-stroke);
  box-shadow: var(--glass-edge), var(--shadow-md);
}
.glass-thin {
  background: var(--glass-bg-thin);
  -webkit-backdrop-filter: var(--glass-blur-thin);
          backdrop-filter: var(--glass-blur-thin);
}
.glass-regular {
  background: var(--glass-bg-regular);
  -webkit-backdrop-filter: var(--glass-blur-regular);
          backdrop-filter: var(--glass-blur-regular);
}
.glass-thick {
  background: var(--glass-bg-thick);
  -webkit-backdrop-filter: var(--glass-blur-thick);
          backdrop-filter: var(--glass-blur-thick);
}

/* ---------- tokens/gradients.css ---------- */
/* Sky gradients — the brand's default backgrounds.
   The "right" surface is rarely a flat color; it's one of these.
   Tagged @kind color so the indexer treats them as palette tokens. */

:root {
  /* ── Times of day */
  --gradient-morning:
    linear-gradient(180deg, #EAF5FF 0%, #CFE8FF 35%, #9DD2FF 70%, #FFE980 100%); /* @kind color */
  --gradient-noon:
    linear-gradient(180deg, #66B7FF 0%, #9DD2FF 50%, #EAF5FF 100%); /* @kind color */
  --gradient-afternoon:
    linear-gradient(180deg, #0A84FF 0%, #66B7FF 60%, #CFE8FF 100%); /* @kind color */
  --gradient-sunset:
    linear-gradient(180deg, #FFB199 0%, #FFE34D 45%, #9DD2FF 100%); /* @kind color */
  --gradient-twilight:
    linear-gradient(180deg, #004690 0%, #006FE0 50%, #FFB199 100%); /* @kind color */

  /* ── Product / abstract — diagonal "soda can" energy */
  --gradient-soda:
    linear-gradient(135deg, #0A84FF 0%, #66B7FF 50%, #EAF5FF 100%); /* @kind color */
  --gradient-ion:
    linear-gradient(135deg, #339BFF 0%, #5FE3CC 100%); /* @kind color */
  --gradient-citrus:
    linear-gradient(135deg, #FFE34D 0%, #FFB199 100%); /* @kind color */

  /* ── Radial — for spotlight / sun glare */
  --gradient-sunspot:
    radial-gradient(ellipse at 70% 20%, #FFE980 0%, rgba(255,233,128,0) 50%),
    linear-gradient(180deg, #66B7FF 0%, #EAF5FF 100%); /* @kind color */
  --gradient-droplet:
    radial-gradient(circle at 30% 30%, rgba(255,255,255,0.9) 0%,
                                       rgba(255,255,255,0) 40%),
    linear-gradient(135deg, #0A84FF 0%, #66B7FF 100%); /* @kind color */

  /* ── Mesh — multi-stop, soft */
  --gradient-mesh-summer:
    radial-gradient(at 20% 10%, #FFE34D 0%, transparent 40%),
    radial-gradient(at 80% 20%, #5FE3CC 0%, transparent 40%),
    radial-gradient(at 50% 80%, #66B7FF 0%, transparent 50%),
    #EAF5FF; /* @kind color */
}

/* ---------- tokens/motion.css ---------- */
/* Motion — calm, springy, water-like.
   No hard linear lerps. Hover is fast (140ms); page transitions are slow
   (420ms) so glass refraction reads. */

:root {
  /* ── Durations */
  --dur-instant: 80ms;    /* @kind other */
  --dur-fast:    140ms;   /* @kind other */
  --dur-base:    240ms;   /* @kind other */
  --dur-slow:    420ms;   /* @kind other */
  --dur-slower:  640ms;   /* @kind other */

  /* ── Easings */
  --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);     /* @kind other */
  --ease-out-expo:  cubic-bezier(0.16, 1, 0.3, 1);     /* @kind other */
  --ease-in-out:    cubic-bezier(0.65, 0, 0.35, 1);    /* @kind other */
  --ease-spring:    cubic-bezier(0.34, 1.56, 0.64, 1); /* @kind other */
  --ease-bounce:    cubic-bezier(0.68, -0.6, 0.32, 1.6); /* @kind other */

  /* ── Composed */
  --transition-hover: all var(--dur-fast) var(--ease-out-quart);   /* @kind other */
  --transition-press: transform var(--dur-instant) var(--ease-out-quart); /* @kind other */
  --transition-glass: backdrop-filter var(--dur-slow) var(--ease-out-expo),
                      background     var(--dur-slow) var(--ease-out-expo); /* @kind other */
}

/* ---------- tokens/semantic.css ---------- */
/* Semantic aliases — what the rest of the system should reference.
   Components should NEVER consume --blue-500 directly; they reference --brand,
   --text-body, --surface-card, etc. */

:root {
  /* ── Brand */
  --brand:           var(--blue-500);
  --brand-hover:     var(--blue-600);
  --brand-press:     var(--blue-700);
  --brand-tint:      var(--blue-50);
  --brand-soft:      var(--blue-100);
  --accent:          var(--sun-400);
  --accent-hover:    var(--sun-500);

  /* ── Text */
  --text-strong:     var(--neutral-800);
  --text-body:       var(--neutral-700);
  --text-muted:      var(--neutral-500);
  --text-faint:      var(--neutral-400);
  --text-on-brand:   var(--neutral-0);
  --text-on-accent:  var(--neutral-800);
  --text-accent:     var(--blue-600);
  --text-danger:     var(--danger-500);

  /* ── Surfaces (solid) */
  --surface-page:        var(--neutral-0);
  --surface-page-tint:   var(--blue-50);
  --surface-elevated:    var(--neutral-0);
  --surface-sunken:      var(--neutral-50);
  --surface-inverse:     var(--neutral-900);

  /* ── Surfaces (glass) — these are the defaults across the brand. */
  --surface-card:        var(--glass-bg-regular);
  --surface-card-blur:   var(--glass-blur-regular);
  --surface-overlay:     var(--glass-bg-thick);
  --surface-overlay-blur: var(--glass-blur-thick);
  --surface-nav:         var(--glass-bg-thin);
  --surface-nav-blur:    var(--glass-blur-thin);

  /* ── Strokes */
  --stroke-hairline: rgba(10, 68, 140, 0.08);
  --stroke-subtle:   rgba(10, 68, 140, 0.12);
  --stroke-strong:   rgba(10, 68, 140, 0.24);
  --stroke-glass:    var(--glass-stroke);

  /* ── State */
  --state-hover-tint:  rgba(10, 132, 255, 0.06);
  --state-press-tint:  rgba(10, 132, 255, 0.12);
  --state-selected:    rgba(10, 132, 255, 0.10);
  --state-disabled-fg: var(--neutral-400);
  --state-disabled-bg: var(--neutral-100);
}


/* ============================================================
   aozora - world layout (phase 1 + polish 1-2 + scroll dimension)
   ------------------------------------------------------------
   Round 3 changes the container grammar:
     - .yo-stage stays viewport-fixed (contract) but now SCROLLS
       inside, along the Y axis, with hidden X.
     - Two viewport-glued layers sandwich the scroll:
         .atmosphere-bg (behind) — sky, cumulus, glare, mist.
         .atmosphere-fg (front)  — fog, rain, storm, veil, wisps.
       WEATHER STAYS ON THE VIEWPORT: rain drops hit the glass, not
       the page. Scroll moves the world behind the pane.
     - .stage-scroll holds the scroll flow: .screen-1 (hero /
       eyebrow / caption / scroll hint) then .aozora-section
       (essays: big word 随筆 + floating entries on 4px baseline).
     - The sun is viewport-fixed as a stage sibling. On scroll it
       parallaxes at 0.3× toward a top-right DOCK, staying reachable
       so the future essay time-filter still has a knob. While
       dragging it snaps back to its arc.
     - Parallax multipliers (--scroll-y in px, and per-plane
       computed offsets) are set by world.js and consumed via
       transform: translateY(var(--parallax)) on the relevant nodes.
   Everything above the 'world layout' comment is the DS token layer
   (unchanged from round 2); everything below is this world's own.
   ============================================================ */

/* ---------- self-hosted fonts (see fonts/LICENSE-*) ---------- */
@font-face {
  font-family: "Inter";
  src: url("fonts/Inter-latin-900.woff2") format("woff2");
  font-weight: 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Inter";
  src: url("fonts/Inter-latin-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Noto Sans JP";
  src: url("fonts/NotoSansJP-500-sub.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Noto Sans JP";
  src: url("fonts/NotoSansJP-700-sub.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
/* Micro subset for the drag hint: 拖 一 太 阳 (~1.3KB).
   Aozora's audience is Chinese-primary; JP subset covers 蒼い空 only. */
@font-face {
  font-family: "Noto Sans SC";
  src: url("fonts/NotoSansSC-500-sub.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+4E00, U+592A, U+62D6, U+9633;
}

/* Round-2 serif candidates for the hero + caption.
   Both faces ship (~7-10KB each after subset to A-Z a-z 0-9 . , : - / · — → space)
   so Yoru can flip between them with ?serif=bodoni|playfair. Default is
   Bodoni Moda. RIFT owns DM Serif Display and Shippori Mincho; those are
   OFF-limits here. */
@font-face {
  font-family: "Bodoni Moda";
  src: url("fonts/BodoniModa-900-sub.woff2") format("woff2");
  font-weight: 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0020, U+002C-002F, U+003A, U+0030-0039, U+0041-005A, U+0061-007A, U+00B7, U+2014, U+2192;
}
@font-face {
  font-family: "Bodoni Moda";
  src: url("fonts/BodoniModa-700-sub.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0020, U+002C-002F, U+003A, U+0030-0039, U+0041-005A, U+0061-007A, U+00B7, U+2014, U+2192;
}
@font-face {
  font-family: "Playfair Display";
  src: url("fonts/PlayfairDisplay-900-sub.woff2") format("woff2");
  font-weight: 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0020, U+002C-002F, U+003A, U+0030-0039, U+0041-005A, U+0061-007A, U+00B7, U+2014, U+2192;
}
@font-face {
  font-family: "Playfair Display";
  src: url("fonts/PlayfairDisplay-700-sub.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0020, U+002C-002F, U+003A, U+0030-0039, U+0041-005A, U+0061-007A, U+00B7, U+2014, U+2192;
}
/* Noto Serif JP — matching mincho for the 蒼い空 gloss + section marks.
   Three tiny 1KB subsets, one per glyph (蒼 U+84BC, 空 U+7A7A, い U+3044).
   Round 3 adds two more per-glyph subsets for the 随筆 section mark
   (随 U+968F, 筆 U+7B46). Follow the same pattern as Noto Sans JP:
   narrow unicode-range so the browser only pulls what it needs. */
@font-face {
  font-family: "Noto Serif JP";
  src: url("fonts/NotoSerifJP-700-a.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+84BC;
}
@font-face {
  font-family: "Noto Serif JP";
  src: url("fonts/NotoSerifJP-700-b.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+7A7A;
}
@font-face {
  font-family: "Noto Serif JP";
  src: url("fonts/NotoSerifJP-700-c.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+3044;
}
@font-face {
  font-family: "Noto Serif JP";
  src: url("fonts/NotoSerifJP-700-d.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+968F;   /* 随 */
}
@font-face {
  font-family: "Noto Serif JP";
  src: url("fonts/NotoSerifJP-700-e.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+7B46;   /* 筆 */
}
/* Noto Sans JP essays subsets: fontsource splits Sans JP into 120+
   chunks by unicode-range, and the characters this section needs fall
   into four different chunks — so four narrow @font-face blocks, one
   per chunk. Every file is <=3KB.
     hira → hiragana used in 「まだ何もない」+ dialogue partner 「あかり」
            (あ か な ま だ も り)
     nani → 何 (U+4F55)
     zui  → 随 (U+968F) — sans fallback for the scroll hint
     fude → 筆 (U+7B46) — sans fallback for the scroll hint */
@font-face {
  font-family: "Noto Sans JP";
  src: url("fonts/NotoSansJP-500-essays-hira.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+3042, U+304B, U+306A, U+307E, U+3060, U+3082, U+308A;
}
@font-face {
  font-family: "Noto Sans JP";
  src: url("fonts/NotoSansJP-500-essays-nani.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+4F55;
}
@font-face {
  font-family: "Noto Sans JP";
  src: url("fonts/NotoSansJP-500-essays-zui.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+968F;
}
@font-face {
  font-family: "Noto Sans JP";
  src: url("fonts/NotoSansJP-500-essays-fude.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+7B46;
}

/* Hero-serif variable: default Bodoni; ?serif=playfair swaps in Playfair
   Display. */
:root {
  --font-hero: "Bodoni Moda", "Noto Serif JP", "Songti SC", "STSong", serif;
}
html[data-serif="playfair"] {
  --font-hero: "Playfair Display", "Noto Serif JP", "Songti SC", "STSong", serif;
}
html[data-serif="bodoni"] {
  --font-hero: "Bodoni Moda", "Noto Serif JP", "Songti SC", "STSong", serif;
}

/* ---------- baseline ---------- */
html, body { height: 100%; margin: 0; }
html { background: var(--blue-200); }   /* base sky tone so morphing snapshots are not white */
/* Body stays overflow: hidden; scrolling lives inside .yo-stage (contract). */
body { overflow: hidden; font-family: var(--font-sans); color: var(--text-strong); }

/* ---------- stage ----------
   Round-3: the stage still fills the viewport (view-transition shared box)
   but now scrolls INTERNALLY on the Y axis. All viewport-glued furniture
   (atmosphere-bg, atmosphere-fg, sun, hint, white) is position:fixed so it
   ignores the scroll; only .stage-scroll's content moves. Scroll behavior
   is smooth+damped through generous section heights and CSS parallax
   ratios — NEVER through wheel/touch hijacking. */
.yo-stage {
  position: fixed;
  inset: 0;
  overflow-x: hidden;
  overflow-y: auto;
  background: var(--gradient-noon);      /* fallback first paint before sky layers wire up */
  color: var(--text-strong);
  font-family: var(--font-display);
  isolation: isolate;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  scrollbar-width: none;
  scroll-behavior: auto;
}
.yo-stage::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* ---------- atmosphere containers ----------
   Both are position:fixed so they sit on the viewport, not on the scroll
   flow. bg lives behind, fg on top of the scroll flow. Neither takes
   pointer events; the sun (separate viewport-fixed sibling) is the only
   interactive element in the atmosphere layers. */
.atmosphere-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  contain: layout paint;
}
.atmosphere-fg {
  position: fixed;
  inset: 0;
  z-index: 10;
  pointer-events: none;
  overflow: hidden;
}

/* Far-plane parallax: clouds (and glare + mist) shift up slowly with
   scroll — the sky gradient itself STAYS put (sky = time-of-day, not a
   plane). --parallax-far is set by world.js on <html> as a negative
   pixel value (e.g. -80px at scrollTop 260px). Only the cloud planes,
   the sun glare, and the horizon mist take the translate. */
.cloud-plane,
.glare,
.mist {
  transform: translate3d(0, var(--parallax-far, 0px), 0);
  will-change: transform;
}

/* ---------- sky (5 crossfading gradient layers) ---------- */
.sky {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.sky-layer {
  position: absolute;
  inset: 0;
  opacity: 0;
  will-change: opacity;
  transition: opacity 320ms var(--ease-out-expo);
}
.sky-twilight  { background: var(--gradient-twilight); }
.sky-morning   { background: var(--gradient-morning); }
.sky-noon      { background: var(--gradient-noon); }
.sky-afternoon { background: var(--gradient-afternoon); }
.sky-sunset    { background: var(--gradient-sunset); }

/* ---------- cumulus ----------
   Unchanged from round 2 aesthetically. Positioning still uses viewport-
   relative units because .atmosphere-bg is fixed to the viewport; the drift
   keyframe stays the same. Scroll parallax is applied at the container
   level (see .atmosphere-bg's transform above). */
.cloud-plane {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 1;
}
.cloud-plane-front { z-index: 3; }

.cumulus {
  position: absolute;
  top: var(--y, 20vh);
  width: var(--w, 34vw);
  height: auto;
  aspect-ratio: 2 / 1;
  transform: translateX(var(--from, -20vw));
  animation:
    cumulus-drift calc(var(--dur, 300s) / var(--wind-mult, 1)) linear infinite;
  animation-delay: var(--delay, 0s);
  opacity: var(--cumulus-op, 1);
  will-change: transform;
  filter: drop-shadow(0 6px 20px rgba(10, 68, 140, 0.10));
}
.cumulus svg {
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
  filter: blur(1.2px);
}
.cloud-plane-front .cumulus { filter: drop-shadow(0 10px 28px rgba(10, 68, 140, 0.14)); }
@keyframes cumulus-drift {
  0%   { transform: translateX(var(--from, -20vw)); }
  100% { transform: translateX(var(--to,   120vw)); }
}

/* Wind wisps (injected by world.js when wind condition is on). */
.wisp {
  position: absolute;
  top: var(--y, 30vh);
  width: var(--w, 12vw);
  height: auto;
  aspect-ratio: 3 / 1;
  opacity: 0.72;
  transform: translateX(var(--from, -20vw));
  animation: cumulus-drift calc(var(--dur, 24s) / var(--wind-mult, 1)) linear infinite;
  animation-delay: var(--delay, 0s);
  will-change: transform;
  pointer-events: none;
}
.wisp svg { width: 100%; height: 100%; display: block; overflow: visible; }

/* ---------- sun glare ---------- */
.glare {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    radial-gradient(
      circle at var(--sun-x, 50%) var(--sun-y, 30%),
      rgba(255, 250, 225, var(--glare-inner, 0.55)) 0%,
      rgba(255, 233, 128, var(--glare-mid, 0.28)) 12%,
      rgba(255, 233, 128, 0.08) 26%,
      rgba(255, 233, 128, 0) 46%);
  mix-blend-mode: screen;
  opacity: var(--glare-op, 0.9);
  transition: opacity 320ms var(--ease-out-expo);
}

/* Horizon mist — thickens at low sun; warm-tinted at sunset. Bottom band. */
.mist {
  position: absolute;
  left: 0; right: 0;
  bottom: 0;
  height: 40vh;
  z-index: 2;
  pointer-events: none;
  background:
    linear-gradient(
      180deg,
      rgba(255, 233, 128, 0) 0%,
      rgba(255, 233, 128, calc(0.06 * var(--mist-warm, 0))) 30%,
      rgba(255, 177, 153, calc(0.10 * var(--mist-warm, 0))) 60%,
      rgba(255, 177, 153, calc(0.16 * var(--mist-warm, 0))) 90%),
    linear-gradient(
      180deg,
      rgba(234, 245, 255, 0) 0%,
      rgba(234, 245, 255, calc(0.10 * var(--mist, 0))) 55%,
      rgba(207, 232, 255, calc(0.18 * var(--mist, 0))) 100%);
  transition: opacity 480ms var(--ease-out-expo);
}

/* ---------- weather: fog v2 + rain-on-glass + storm + veil ----------
   All live inside .atmosphere-fg, glued to the viewport (position:fixed
   parent). Everything below is unchanged aesthetically from round 2; the
   only difference is which container they're in. */
/* .fog is now pure atmospheric HAZE at low opacity — never the spectacle
   band Yoru vetoed. The gradient shape is unchanged; the change is that
   `--fog-op` no longer climbs to 0.85 for the fog condition. Rain / storm
   still consume this element at their previous 0.10 / 0.18 haze levels,
   pixel-equivalent to round 3. The old horizon-spectacle backdrop-filter
   is gone (round-3 kept a 3px blur scaled to --fog-op — with fog-op
   capped at 0.18 now that blur reaches 0.54px, imperceptible either way)
   which leaves Aozora with zero backdrop-filter on the atmospheric layers. */
.fog {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  background:
    linear-gradient(
      180deg,
      rgba(136, 158, 186, 0) 0%,
      rgba(136, 158, 186, 0) 42%,
      rgba(122, 148, 180, 0.18) 60%,
      rgba(110, 138, 172, 0.55) 78%,
      rgba(126, 152, 184, 0.42) 100%),
    radial-gradient(
      ellipse at 50% 88%,
      rgba(158, 180, 208, 0.35) 0%,
      rgba(158, 180, 208, 0) 60%);
  opacity: var(--fog-op, 0);
  transition: opacity 800ms var(--ease-out-expo);
}

.rain {
  position: absolute;
  inset: 0;
  z-index: 12;
  pointer-events: none;
  opacity: var(--rain-op, 0);
  transition: opacity 640ms var(--ease-out-expo);
}
.rain-drop {
  position: absolute;
  transform: translate3d(-50%, -50%, 0);
  will-change: transform, opacity;
  filter: drop-shadow(0 1px 1px rgba(10, 68, 140, 0.22));
}
.rain-drop.rain-runner {
  -webkit-backdrop-filter: blur(1.5px);
          backdrop-filter: blur(1.5px);
  border-radius: 50%;
}

.scene-dim {
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  background: var(--dim-color, rgb(10, 40, 70));
  opacity: var(--scene-dim, 0);
  mix-blend-mode: multiply;
  transition: opacity 640ms var(--ease-out-expo),
              background 640ms var(--ease-out-expo);
}

/* Sheet flash: round-4. Background is set per-event by JS to a radial
   gradient (spatial origin + amplitude vary per strike). Opacity is
   JS-driven through a multi-sub-pulse envelope. mix-blend-mode:screen
   makes the transparent surround invisible so the shape is what shows —
   no rectangle-clipped edges. Photosensitivity hard cap (0.55) is
   enforced in JS per frame. */
.sheet-flash {
  position: absolute;
  inset: 0;
  z-index: 7;
  pointer-events: none;
  background: transparent;
  mix-blend-mode: screen;
  opacity: 0;
  will-change: opacity, background;
}

.bolt-layer {
  position: absolute;
  inset: 0;
  z-index: 8;
  pointer-events: none;
  overflow: visible;
}
.bolt-layer svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
/* Bolt: JS-driven opacity flicker (~420 ms envelope). No CSS animation —
   the flicker envelope now lives in strikeBolt() so the crackle can vary
   per strike rather than looping through the same keyframes each time.
   Stroke stays near-white with a cool drop-shadow chain. */
.bolt {
  fill: none;
  stroke: #FDFCFF;
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter:
    drop-shadow(0 0 8px rgba(200, 220, 255, 0.85))
    drop-shadow(0 0 22px rgba(150, 190, 255, 0.55));
  opacity: 0;
}
/* Echo (double-expose) bolt: slightly dimmer, thinner glow, fires ~40 ms
   after the main channel — reads as the ionisation flickering back to
   life through the same corridor. */
.bolt.bolt-echo {
  stroke-width: 2.0;
  filter:
    drop-shadow(0 0 5px rgba(200, 220, 255, 0.7))
    drop-shadow(0 0 14px rgba(150, 190, 255, 0.4));
  opacity: 0;
}
.bolt-glow {
  fill: none;
  stroke: rgba(200, 220, 255, 0.35);
  stroke-width: 10;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
}

/* ---------- cloud-veil (clouds condition drifts a puff across hero) ---- */
.cloud-veil {
  position: absolute;
  left: 0;
  top: 42vh;
  width: 46vw;
  height: 30vh;
  z-index: 6;
  pointer-events: none;
  opacity: 0;
  transform: translateX(-40vw);
  filter: drop-shadow(0 8px 26px rgba(10, 68, 140, 0.12));
  transition: opacity 640ms var(--ease-out-expo);
}
.cloud-veil svg { width: 100%; height: 100%; display: block; overflow: visible; }
body[data-weather="clouds"] .cloud-veil { opacity: 0.94; }
/* Fog reuses the clouds veil so the sky wears the overcast face. Combined
   with WX.fog's dim 0.20 + glare 0.35 + fog 0.16 haze, the effect reads as
   an overcast fog day; the corner caption stays honest ("fog"). */
body[data-weather="fog"] .cloud-veil { opacity: 0.94; }

/* ---------- horizon: still functional-only ---------- */
.horizon { display: none; }

/* ============================================================
   Scroll flow: screen 1 + sections
   ------------------------------------------------------------ */

/* .stage-scroll is the only element in the flow. Sections stack inside it.
   z-index above atmosphere-bg (0), below atmosphere-fg (10). */
.stage-scroll {
  position: relative;
  z-index: 5;
  width: 100%;
  min-height: 100vh;
  padding: 0;
  margin: 0;
}

/* ---------- screen 1 ----------
   Same visual contract as rounds 1–2. Hero + eyebrow + caption keep their
   pixel positions. Height locked to 100vh so scrolling past reveals the
   next section. Nothing inside screen-1 is a placeholder; every piece is
   the round-2 element in a new home. */
.screen-1 {
  position: relative;
  height: 100vh;
  width: 100%;
  overflow: visible;
  pointer-events: none;   /* content inside re-enables where needed */
}

/* ---------- hero (200px word + CJK gloss) ----------
   Position matches round 2 exactly. Entrance still governed by
   html[data-open]. Now translated by --parallax-mid (1×, applied
   as translateY so the hero scrolls at page speed alongside the flow). */
.hero {
  position: absolute;
  left: 6vw;
  bottom: 14vh;
  z-index: 4;
  pointer-events: none;
  color: var(--neutral-900);
  --hero-shadow: 6px 6px 24px rgba(10, 68, 140, 0.14);
  --hero-wet-shadow: 0 0 0 rgba(0, 0, 0, 0);
  --hero-wet-highlight: 0 0 0 rgba(255, 255, 255, 0);
  --hero-strike-glow: 0 0 0 rgba(0, 0, 0, 0);
  --hero-blur: 10px;
  --hero-lift: 14px;
  filter: blur(var(--hero-blur));
  transform: translateY(var(--hero-lift));
  opacity: 0.001;
  transition:
    filter 720ms var(--ease-out-expo) 240ms,
    transform 900ms var(--ease-out-expo) 240ms,
    opacity 640ms var(--ease-out-expo) 320ms;
}
html[data-open] .hero {
  --hero-blur: 0px;
  --hero-lift: 0px;
  opacity: 1;
}
.hero-latin {
  display: block;
  font-family: var(--font-hero);
  font-weight: 900;
  font-size: clamp(120px, 14vw, 200px);
  line-height: 0.9;
  letter-spacing: -0.015em;
  text-shadow:
    var(--hero-wet-highlight),
    var(--hero-strike-glow),
    var(--hero-wet-shadow),
    var(--hero-shadow);
  filter: var(--hero-filter, none);
  transition: filter 380ms var(--ease-out-expo);
}
.hero-latin .char {
  display: inline-block;
  transform: translate(var(--sx, 0px), var(--sy, 0px)) rotate(var(--sr, 0deg));
  transform-origin: 50% 100%;
  will-change: transform;
  transition: transform 240ms var(--ease-out-quart);
}
.hero-gloss {
  display: block;
  font-family: "Noto Serif JP", var(--font-hero);
  font-weight: 700;
  font-size: clamp(40px, 4.4vw, 64px);
  line-height: 1.05;
  letter-spacing: 0;
  margin-top: 14px;
  color: var(--neutral-800);
  text-shadow: 3px 3px 12px rgba(10, 68, 140, 0.08);
}
.hero-gloss .char {
  display: inline-block;
  transform: translate(var(--sx, 0px), var(--sy, 0px)) rotate(var(--sr, 0deg));
  transform-origin: 50% 100%;
  will-change: transform;
  transition: transform 240ms var(--ease-out-quart);
}
body[data-weather="rain"] .hero,
body[data-weather="storm"] .hero {
  --hero-wet-highlight: 0 -1px 0 rgba(255, 255, 255, 0.65);
  --hero-wet-shadow:    0 2px 4px rgba(6, 20, 40, 0.55);
  --hero-filter: brightness(0.94) contrast(1.05) saturate(1.08);
}
.hero.strike {
  --hero-strike-glow:
    0 0 6px rgba(255, 255, 255, 0.98),
    0 0 18px rgba(220, 235, 255, 0.9),
    0 0 42px rgba(180, 210, 255, 0.6);
  animation: strike-flash 380ms var(--ease-out-quart) 1;
}
@keyframes strike-flash {
  0%   { filter: brightness(1);   }
  20%  { filter: brightness(2.4); }
  100% { filter: brightness(1);   }
}
body[data-weather="clouds"] .hero {
  --hero-shadow: 8px 8px 26px rgba(10, 68, 140, 0.18);
}

/* ---------- eyebrow (vertical right edge) ----------
   Round-3: split into per-item spans so JS can highlight the current
   section as the reader scrolls. Neutral resting state; active item gets
   more ink. Still aria-hidden decoration (the aside is corner-note).
   Viewport-FIXED, not in the scroll flow: it is the section index, so it
   must still be on screen when the section it points at is — otherwise
   the highlight can never be seen. */
.eyebrow {
  position: fixed;
  right: 20px;
  top: 24px;
  bottom: 88px;
  z-index: 4;
  writing-mode: vertical-rl;
  text-orientation: mixed;
  font-family: "Noto Sans JP", "Inter", sans-serif;
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.32em;
  color: var(--neutral-500);
  /* flex, not grid: the item/sep spans must flow as ONE vertical line.
     Grid auto-placement gives every span its own track, which renders
     as side-by-side columns in vertical-rl. */
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  user-select: none;
}
.eyebrow-item {
  color: inherit;
  transition: color 240ms var(--ease-out-expo);
}
.eyebrow-item.is-active { color: var(--neutral-900); }
.eyebrow-sep { color: inherit; opacity: 0.6; }

/* ---------- corner caption ----------
   Near-plane parallax caption: rides the front pane, drifting up faster
   than the hero as reader scrolls. --parallax-near is set by JS. Also
   fades out via --caption-op as the reader leaves screen 1 — a
   readability aid that works in reduce-motion too (so the docked sun
   has a clean corner). */
.caption {
  position: absolute;
  right: 22px;
  bottom: 18px;
  z-index: 9;
  display: flex;
  align-items: baseline;
  gap: 0 12px;
  color: var(--neutral-900);
  font-family: var(--font-hero);
  font-weight: 700;
  font-size: 14px;
  line-height: 1.2;
  letter-spacing: 0.02em;
  text-shadow: 0 1px 3px rgba(255, 255, 255, 0.55),
               0 0 12px rgba(255, 255, 255, 0.35);
  pointer-events: none;
  user-select: none;
  font-feature-settings: "lnum" 1, "tnum" 1;
  transform: translateY(var(--parallax-near, 0px));
  opacity: var(--caption-op, 1);
  transition: opacity 320ms var(--ease-out-expo);
  will-change: transform, opacity;
}
.caption > span { display: inline-block; }
.caption .cap-sep {
  color: var(--neutral-500);
  opacity: 0.7;
  font-weight: 400;
}
body[data-weather="storm"] .caption,
body[data-weather="rain"] .caption {
  color: #F1F5FA;
  text-shadow: 0 1px 4px rgba(4, 14, 30, 0.6),
               0 0 12px rgba(4, 14, 30, 0.45);
}
body[data-weather="storm"] .caption .cap-sep,
body[data-weather="rain"] .caption .cap-sep { color: #B9C4D0; }

/* ---------- scroll hint ----------
   A diegetic 'there is more below' whisper — small serif at the horizon
   band, centered-lower. The section-word (随筆) sits above a mono-ish
   arrow so the reader reads what they'll find, then what to do. Fades
   out linearly as scroll begins (opacity var driven by JS). Reduced
   motion keeps it static (no bounce, no pulse).
   Rationale: NOT a "scroll down" banner, NOT a bouncing chevron pill —
   it's the same corner-note voice the caption uses, extended for one
   more line to point at what's below. */
.scroll-hint {
  position: absolute;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  color: var(--neutral-900);
  pointer-events: none;
  user-select: none;
  opacity: var(--scroll-hint-op, 0.86);
  transition: opacity 320ms var(--ease-out-expo);
  text-shadow: 0 1px 3px rgba(255, 255, 255, 0.55),
               0 0 10px rgba(255, 255, 255, 0.35);
}
.scroll-hint-cjk {
  font-family: "Noto Serif JP", var(--font-hero);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: 0.06em;
  line-height: 1;
}
.scroll-hint-arrow {
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1;
  opacity: 0.7;
  margin-top: 2px;
}
body[data-weather="storm"] .scroll-hint,
body[data-weather="rain"] .scroll-hint {
  color: #F1F5FA;
  text-shadow: 0 1px 4px rgba(4, 14, 30, 0.6),
               0 0 12px rgba(4, 14, 30, 0.45);
}

/* ============================================================
   ESSAYS SECTION (screen 2)
   ------------------------------------------------------------
   The concept: 随笔是飘在空中的词. Entries are NOT cards; they are
   short bodies of text at scattered baseline positions, each with
   a bare timestamp. Regular-tier glass rims give them the DS's
   'the glass is always on color' feel without turning them into
   tiles. When essays.json is empty (the default), only the section
   mark + one quiet empty-state caption render.
   The section mark is one BIG word — 随筆 CJK primary + tiny latin
   gloss above — following the DS rule 'headline or caption, almost
   nothing between'.
   ============================================================ */

.aozora-section {
  position: relative;
  width: 100%;
  min-height: 100vh;
  padding: 12vh 6vw 12vh;
  pointer-events: auto;   /* re-enable interaction inside the flow */
  box-sizing: border-box;
}

/* section-mark: a header where the CJK word carries most of the weight,
   the latin gloss sits above smaller like an editorial superscript.
   The 'one big word per screen' rule from the concept. */
.section-mark {
  display: block;
  position: relative;
  z-index: 2;
  color: var(--neutral-900);
  text-shadow: 0 2px 12px rgba(255, 255, 255, 0.4);
}
.section-latin {
  display: block;
  font-family: var(--font-hero);
  font-weight: 700;
  font-size: clamp(18px, 1.6vw, 22px);
  letter-spacing: 0.32em;
  text-transform: lowercase;
  color: var(--neutral-500);
  margin-bottom: 10px;
}
.section-cjk {
  display: block;
  font-family: "Noto Serif JP", var(--font-hero);
  font-weight: 700;
  font-size: clamp(120px, 16vw, 220px);
  line-height: 0.9;
  letter-spacing: 0.02em;
  color: var(--neutral-900);
  text-shadow: 6px 6px 24px rgba(10, 68, 140, 0.14);
}

/* section-words: container for entries. If empty (empty-state), shows
   a single caption. If populated, entries are absolute-positioned on
   an invisible 4px baseline (--x --y set per entry by JS). Generous
   margin-top so the first entry never overlaps the 随筆 mark's descender. */
.section-words {
  position: relative;
  margin-top: 96px;
  min-height: 320px;
}

/* empty state — a quiet honest caption, corner-note voice */
.section-empty {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-top: 40px;
  color: var(--neutral-500);
  text-shadow: 0 1px 3px rgba(255, 255, 255, 0.5);
  font-feature-settings: "lnum" 1;
}
.section-empty .empty-cjk {
  font-family: "Noto Sans JP", var(--font-sans);
  font-weight: 500;
  font-size: 16px;
  letter-spacing: 0.08em;
  color: var(--neutral-600);
}
.section-empty .empty-latin {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: lowercase;
  color: var(--neutral-400);
}

/* Populated: the section becomes taller to fit all entries. When JS
   detects entries it sets a container height matching the tallest
   entry position + padding. */
.section-words.is-populated {
  min-height: var(--words-height, 720px);
}

/* Per-entry glass chip — DS 'regular' tier for essays. Sized to text,
   scattered position, rim highlight so it reads as floating glass and
   not a card. --x is a viewport-width percent (0..100), --y is px on
   the 4px baseline. Timestamp mono, body serif or sans by kind.
   Entries scroll at MID plane (1×) with the section — near-plane extra
   drift is applied only to the .word-meta (timestamps) inside each
   entry, since 'caption' is the near plane in the DS spec. */
.word-entry {
  position: absolute;
  top: var(--y, 0px);
  left: var(--x, 12%);
  max-width: var(--max-w, 32ch);
  padding: 14px 20px 12px;
  background: var(--glass-bg-regular);
  border: 1px solid var(--glass-stroke);
  box-shadow: var(--glass-edge), var(--shadow-sm);
  border-radius: var(--radius-xl);
  -webkit-backdrop-filter: var(--glass-blur-regular);
          backdrop-filter: var(--glass-blur-regular);
  color: var(--neutral-900);
  transition: box-shadow var(--dur-fast) var(--ease-out-quart);
  /* Subtle near-plane drift so the essays glass sits a touch closer to
     the reader than the section mark. Scaled down (--parallax-near is
     0.1× scroll; we take a fraction of it here to keep entries clearly
     anchored to their y position). */
  transform: translateY(calc(var(--parallax-near, 0px) * 0.3));
  will-change: transform;
}
/* right-leaning variants use a --side custom prop so their timestamp sits
   opposite the body */
.word-entry[data-side="r"] {
  text-align: right;
}
.word-entry:hover {
  box-shadow: var(--glass-edge-strong), var(--shadow-md);
}

.word-body {
  display: block;
  font-family: var(--font-hero);
  font-weight: 700;
  font-size: clamp(15px, 1.4vw, 18px);
  line-height: 1.4;
  color: var(--neutral-900);
  letter-spacing: 0.01em;
}
.word-entry[data-kind="dialogue"] .word-body {
  font-family: "Noto Serif JP", var(--font-hero);
  font-weight: 700;
}
.word-entry[data-kind="link"] .word-body {
  font-family: var(--font-hero);
  font-weight: 700;
  font-style: italic;
}

/* .word-meta: mono timestamp + optional 'with' partner or link mark.
   Stays anchored inside its entry (no internal parallax) — the near
   plane on the essays screen is expressed by the entry chip as a whole
   subtly drifting ahead of the section mark. */
.word-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--neutral-500);
  font-feature-settings: "tnum" 1, "lnum" 1;
}
.word-entry[data-side="r"] .word-meta { justify-content: flex-end; }
.word-with {
  color: var(--neutral-600);
  font-family: "Noto Sans JP", var(--font-mono);
  letter-spacing: 0.08em;
  text-transform: none;
}
.word-time { color: var(--neutral-500); }
.word-link {
  color: var(--neutral-500);
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1;
}

/* Long entries get a subtle 'more' cue in the corner of the chip. Not a
   button this round — the reading layer is a future stub — just an
   editorial mark. */
.word-entry[data-long="true"]::after {
  content: "…";
  position: absolute;
  right: 14px;
  bottom: 10px;
  color: var(--neutral-400);
  font-family: var(--font-hero);
  font-weight: 700;
  font-size: 14px;
  line-height: 1;
}

/* ============================================================
   The sun (viewport-fixed sibling)
   ------------------------------------------------------------
   Round-3: position:fixed (was absolute inside stage). Arc math in JS
   still writes `left`/`top` in viewport pixels. On scroll the JS
   ADDITIONALLY interpolates `left`/`top` toward the top-right DOCK
   using scroll-derived progress; while dragging the interpolation
   collapses to 0 and the sun snaps back to the arc for authentic
   physics. Transitions run on left/top so undocking / re-docking
   animates smoothly.
   ============================================================ */
.sun {
  position: fixed;
  width: 88px;
  height: 88px;
  transform: translate(-50%, -50%);
  z-index: 18;
  cursor: grab;
  touch-action: none;
  border-radius: 50%;
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  padding: 0;
  background:
    radial-gradient(circle at 50% 50%,
      #FFFFFF 0%,
      #FFFAE1 20%,
      #FFE980 34%,
      rgba(255, 227, 77, 0.55) 55%,
      rgba(255, 227, 77, 0.16) 76%,
      rgba(255, 227, 77, 0) 100%);
  filter: drop-shadow(0 0 22px rgba(255, 208, 0, 0.55));
  transition: filter var(--dur-fast) var(--ease-out-quart),
              transform 320ms var(--ease-out-expo),
              left 320ms var(--ease-out-expo),
              top 320ms var(--ease-out-expo);
  outline: none;
}
.sun.dragging {
  /* while pointer-dragging, disable position transitions so the sun
     follows the pointer at 60fps; keep filter/transform smooth */
  transition: filter var(--dur-fast) var(--ease-out-quart);
}
.sun::before {
  content: "";
  position: absolute;
  inset: 30%;
  border-radius: 50%;
  background:
    radial-gradient(circle at 40% 35%, #FFFFFF 0%, #FFFAE1 55%, #FFE980 100%);
  box-shadow: 0 0 12px rgba(255, 255, 255, 0.55);
}
.sun:hover { filter: drop-shadow(0 0 30px rgba(255, 208, 0, 0.75)); }
.sun:active,
.sun.dragging { cursor: grabbing; filter: drop-shadow(0 0 36px rgba(255, 208, 0, 0.85)); }
.sun:focus-visible { box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.9), 0 0 0 8px rgba(10, 132, 255, 0.5); }
.sun.gone { opacity: 0; transition: opacity 180ms linear; }

/* ---------- drag hint (viewport-fixed) ---------- */
.hint {
  position: fixed;
  z-index: 19;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 8px 12px 8px 10px;
  color: var(--neutral-900);
  background: rgba(255, 255, 255, 0.62);
  border: 1px solid var(--glass-stroke);
  border-radius: var(--radius-md);
  box-shadow: var(--glass-edge), var(--shadow-sm);
  -webkit-backdrop-filter: blur(8px) saturate(140%);
          backdrop-filter: blur(8px) saturate(140%);
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.96);
  transform-origin: center;
  pointer-events: none;
  transition: opacity 260ms var(--ease-out-expo), transform 260ms var(--ease-out-expo);
  animation: hint-breathe 2400ms var(--ease-out-expo) 900ms 4;
}
html[data-hint="on"] .hint {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}
.hint-arc {
  width: 46px;
  height: 22px;
  color: var(--neutral-700);
  margin-bottom: 2px;
}
.hint-cn {
  font-family: "Noto Sans SC", "PingFang SC", "Hiragino Sans", "Microsoft YaHei", system-ui, sans-serif;
  font-weight: 500;
  font-size: 13px;
  letter-spacing: 0.06em;
  line-height: 1;
  color: var(--neutral-900);
}
.hint-en {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.18em;
  line-height: 1;
  color: var(--neutral-500);
  text-transform: uppercase;
}
@keyframes hint-breathe {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  50%      { transform: translate(-50%, -50%) scale(1.04); }
}

/* ---------- white veil (enter/leave through white) ---------- */
.white {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: #FFFFFF;
  opacity: 1;
  pointer-events: none;
  transition: opacity 720ms var(--ease-out-expo);
}
html[data-open] .white { opacity: 0; }
html[data-yo-leaving] .white { opacity: 1; transition: opacity 480ms var(--ease-out-quart); }

/* ---------- return key: crescent + back arrow, on glass ---------- */
.yo-return {
  top: 20px;
  left: 20px;
  min-width: 44px;
  min-height: 44px;
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: var(--radius-pill);
  color: var(--neutral-900);
  background: var(--glass-bg-regular);
  -webkit-backdrop-filter: var(--glass-blur-regular);
          backdrop-filter: var(--glass-blur-regular);
  border: 1px solid var(--glass-stroke);
  box-shadow: var(--glass-edge), var(--shadow-sm);
  transition: transform var(--dur-instant) var(--ease-out-quart),
              background var(--dur-fast) var(--ease-out-quart);
  z-index: 30;
}
.yo-return svg { display: block; width: 26px; height: 24px; }
.yo-return:hover { background: rgba(255, 255, 255, 0.62); }
.yo-return:active { transform: scale(0.94); }
.yo-return:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; }

/* ---------- view transition (destination decides through-white) ---------- */
html:active-view-transition-type(to-aozora)::view-transition-old(monitor) {
  animation: aozora-out 420ms var(--ease-out-expo) forwards;
}
html:active-view-transition-type(to-aozora)::view-transition-new(monitor) {
  animation: aozora-in 420ms var(--ease-out-expo) forwards;
}
@keyframes aozora-out {
  0%   { opacity: 1; }
  55%  { opacity: 0; }
  100% { opacity: 0; }
}
@keyframes aozora-in {
  0%   { opacity: 0; }
  45%  { opacity: 1; }
  100% { opacity: 1; }
}

/* ---------- 390 wide (mobile) ---------- */
@media (max-width: 640px) {
  .hero { left: 5vw; bottom: 16vh; }
  .hero-latin { font-size: clamp(64px, 20vw, 120px); letter-spacing: -0.005em; }
  .hero-gloss { font-size: clamp(24px, 7vw, 40px); margin-top: 8px; }
  .eyebrow { right: 12px; top: 20px; bottom: 120px; font-size: 10px; letter-spacing: 0.28em; }
  .caption { right: 14px; bottom: 16px; font-size: 12px; gap: 0 8px; }
  .yo-return { top: 16px; left: 16px; width: 42px; height: 42px; }
  .yo-return svg { width: 24px; height: 22px; }
  .sun { width: 72px; height: 72px; }
  .hint-cn { font-size: 12px; }
  .hint-arc { width: 40px; height: 18px; }
  .cumulus { --w: 56vw; }
  .cloud-veil { width: 78vw; top: 46vh; height: 22vh; }
  .scroll-hint { bottom: 18px; }
  .scroll-hint-cjk { font-size: 13px; }
  .scroll-hint-arrow { font-size: 11px; }

  /* essays section on narrow */
  .aozora-section { padding: 8vh 5vw 10vh; }
  .section-latin { font-size: 14px; letter-spacing: 0.28em; }
  .section-cjk { font-size: clamp(72px, 22vw, 120px); }
  .section-words { margin-top: 40px; }
  .word-entry { max-width: 82vw; padding: 12px 16px 10px; }
  .word-body { font-size: 14px; }
}

/* ---------- reduced motion ----------
   Everything at its OPEN state, and every polish-round animation is off
   (no cloud drift, no flash, no wind sway, no strike glow). Storm still
   renders — static dark. Parallax multipliers are neutralized to 0 (see
   the --parallax-* overrides on <html>). */
@media (prefers-reduced-motion: reduce) {
  html {
    --parallax-far: 0px !important;
    --parallax-near: 0px !important;
  }
  .yo-stage { scroll-behavior: auto; }
  .sky-layer,
  .white,
  .sun,
  .mist,
  .glare,
  .fog,
  .rain,
  .hero,
  .scroll-hint,
  .word-entry,
  .yo-return { transition: none !important; }
  .white { opacity: 0 !important; }
  .hero { filter: none !important; transform: none !important; opacity: 1 !important; }
  .hint { animation: none !important; }
  .cumulus, .wisp, .cloud-veil { animation: none !important; }
  .cumulus-a { transform: translateX(6vw)  !important; }
  .cumulus-b { transform: translateX(72vw) !important; }
  .cumulus-c { transform: translateX(20vw) !important; }
  .cumulus-d { transform: translateX(58vw) !important; }
  .sheet-flash { animation: none !important; opacity: 0 !important; }
  .bolt, .bolt-glow { animation: none !important; opacity: 0 !important; }
  .hero.strike { animation: none !important; }
  .hero-latin .char, .hero-gloss .char { transition: none !important; transform: none !important; }
  .atmosphere-bg { transform: none !important; }
  .word-entry { transform: none !important; }
}
