/* =========================================================
   AYAGIRI TAKUYA OFFICIAL PROFILE SITE
   Design concept: HONEY x FOREST x WOOD x STREAMER(accent only)
   Warm ivory base, honey gold as the hero color, wood-brown
   ink, soft paper-like textures. Game/streamer language is a
   restrained UI accent only — never the leading motif.
   ========================================================= */

:root {
  /* -- warm ivory surface -- */
  --bg: #fbf3e4;
  --bg-soft: #f5e9d0;
  --surface: #fffdf8;

  /* -- ink (warm brown, never pure black) -- */
  --ink: #3a2e20;
  --ink-dim: #6b5a44;
  --ink-faint: #9c8b72;

  /* -- honey (hero color) -- */
  --honey-300: #f4cd7c;
  --honey-500: #e8a93b;
  --honey-600: #c67c1a;
  --honey-text: #a8660c;

  /* -- wood -- */
  --wood: #8a6a45;
  --wood-deep: #5c4527;

  /* -- rare accents (used sparingly, tags/hover only) -- */
  --accent-lavender: #c9b8e8;
  --accent-pink: #f2b8c6;
  --accent-sky: #a8d0e6;
  --accent-leaf: #8fae6b;

  /* -- accessible text variants (WCAG AA on --bg / --bg-soft) --
     Same hues as --ink-faint / --honey-text, darkened only as far as
     4.5:1 requires, for the small text that carries information
     (metadata, tab labels, REQUIRED/OPTIONAL, form status). The parent
     tokens above keep their approved values and stay in use for
     decorative text, borders, backgrounds and hover states. -- */
  --ink-faint-text: #756855;   /* 4.92 on --bg · 4.51 on --bg-soft */
  --honey-text-deep: #985c0b;  /* 4.92 on --bg · 4.51 on --bg-soft */

  /* -- honey header-stripe match: sampled directly from the HONEY master
     PNG's own lightest/mid tones, so the TEST BUILD banner reads as the
     same gold flowing behind it rather than a separately-tuned color.
     Not reused as --honey-300/500 (those stay as originally calibrated
     for tags/buttons/etc. elsewhere on the site). -- */
  --honey-header-light: #f6d766;
  --honey-header-mid: #eab93f;

  /* -- structure -- */
  --border: rgba(90, 69, 39, 0.16);
  --border-strong: rgba(198, 124, 26, 0.45);
  --card-shadow: 0 10px 26px rgba(58, 46, 32, 0.08);
  --card-shadow-hover: 0 16px 34px rgba(58, 46, 32, 0.14);

  --font-en-display: "Fraunces", "Shippori Mincho", serif;
  --font-jp-display: "Shippori Mincho B1", serif;
  --font-jp-body: "Zen Maru Gothic", sans-serif;
  --font-label: "Zen Kaku Gothic New", sans-serif;
  --font-mono: "SF Mono", "Zen Kaku Gothic New", monospace;
  /* HERO-only: a modern, slightly condensed bold sans — deliberately not
     the Serif used for section titles, to read as "current talent" over
     "classic hotel brochure" the moment the photo loads */
  --font-hero: "Oswald", "Zen Kaku Gothic New", sans-serif;

  --header-h: 72px;
  --max-w: 1180px;

  color-scheme: light;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  position: relative;
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-jp-body);
  line-height: 1.9;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* F: paper / watercolor texture — kept extremely subtle on purpose.
   Fixed + very low opacity so it never reads as "paper is sitting here",
   just a faint hint of warmth behind the UI. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background-image: url("../assets/graphics/paper-texture-f.webp");
  background-size: cover;
  background-position: center;
  opacity: 0.03;
  pointer-events: none;
}

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; margin: 0; padding: 0; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
/* PHASE W3-A: screen-reader-only text (first use: the WORKS side-switch
   live region). The standard clip pattern — in the DOM and in the
   accessibility tree, off the visual page. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
h1, h2, h3, p, dl, dd { margin: 0; }

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--honey-500);
  /* D-4: was var(--wood-deep), which measured 4.35 on this --honey-500 ground --
     just under AA for 16px body text. --ink is the pair already approved in D-1
     for .contact-submit against the same --honey-500 fill (6.39), so this is a
     reused pair rather than a new judgement. The background is unchanged. */
  color: var(--ink);
  padding: 0.6em 1em;
  z-index: 999;
}
.skip-link:focus { left: 0; }

/* =========================================================
   FIXED HONEY DRIP — Graphic Signature
   FROZEN: the master image (assets/graphics/honey-master-v2.webp,
   user-supplied FINAL master; only its white studio background was keyed to
   transparency — never re-exported/re-cropped/re-colored by code) and its
   3-layer silhouette/color/drip/drop design. It replaced the earlier
   honey-master.png, which is no longer referenced by any page; there is one
   master image and no supplementary/edge-fill layer behind it. Because the
   new master's intrinsic ratio is 1536/1024 (was 1672/941), the desktop
   aspect-ratio further down had to move with it. Desktop coverage is FROZEN at 35vw
   (--honey-coverage) as the approved baseline. What stays open is purely
   layout: scale/position/crop per breakpoint, and the independent
   micro-drop animation below.

   Positioned flush to the true viewport top-right corner (top:0; right:0)
   rather than offset below the TEST BUILD banner — z-index is deliberately
   LOWER than the banner's (50) so the banner simply overlays HONEY's top
   sliver while it exists, with no gap between them. Once the banner is
   removed at real launch (see LAUNCH_CHECKLIST.md), HONEY is already
   sitting exactly at the true top-right edge with nothing to adjust — it
   reads as honey flowing in from off-screen, not an image placed in a
   corner.
   ========================================================= */
:root {
  --honey-coverage: 35vw; /* FROZEN baseline — approved over the 30vw option */
}
.honey-drip {
  position: fixed;
  top: -2px;
  right: -2px;
  /* z-index below .tbd-banner (50): the temporary banner overlays HONEY's
     top sliver instead of HONEY being pushed down to clear it, so there is
     never a visible gap between banner and HONEY, now or once the banner
     is removed. Still above the HERO photo/content beneath it. */
  z-index: 45;
  width: clamp(300px, var(--honey-coverage), 620px);
  pointer-events: none; /* never intercepts nav/link clicks or taps */
  filter: drop-shadow(0 8px 18px rgba(120, 74, 10, 0.16));
}
.honey-drip__img {
  display: block;
  width: 100%;
  height: auto;
}
@media (max-width: 640px) {
  .honey-drip {
    /* independent mobile treatment, not a scaled-down desktop box: a
       fixed-aspect crop window (object-fit:cover) showing the right-hand
       portion of the same PNG, so HONEY reads as flowing in from the
       corner rather than a shrunken rectangle. Width raised ~10% over the
       previously-verified-safe 34vw (→ 37vw) while height is held at the
       previously-verified 20vw so the vertical reach into the Safe Area
       is unchanged. */
    width: 37vw;
    height: 20vw;
    max-width: 210px;
    max-height: 114px;
    overflow: hidden;
  }
  .honey-drip__img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: 82% 0%;
  }
}

/* ---------- HONEY drop micro-animation ----------
   Independent element, never the body itself. Spawned/removed by
   js/main.js at a fixed anchor point (percentage of the HONEY box, so it
   scales with responsive width) near one of the master PNG's own drip
   tips. One drop at a time, modest fall, natural ease-in acceleration,
   fades out — see js/main.js "HONEY drop" for timing/frequency. */
.honey-drop-anchor {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.honey-drop {
  position: absolute;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--honey-header-light), var(--honey-500) 70%, var(--honey-600));
  pointer-events: none;
  animation: honeyDropFall 1.5s cubic-bezier(0.55, 0, 0.85, 0.35) forwards;
}
@keyframes honeyDropFall {
  0% { transform: translateY(0) scale(1); opacity: 0.95; }
  65% { opacity: 0.8; }
  100% { transform: translateY(46px) scale(0.85); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .honey-drop { display: none; }
}

/* =========================================================
   SITE-WIDE FIXED UI: MENU (top-left) — the only other
   persistent element besides the honey-drip. Everything else
   (brand mark, nav links, CONTACT button) lives inside the
   overlay instead of a permanent header bar.
   ========================================================= */
.menu-trigger {
  position: fixed;
  /* PATCH 007.3: nudged up and left (50px -> 40px, 14px -> 10px) so the
     control reads as a screen-level fixture balanced against the honey drip
     in the opposite corner, rather than something floating inside the HERO.
     The top safe-area inset is the existing architecture and is kept as it
     was; the left one is added for the same reason now that the control sits
     closer to the edge -- it is 0 in portrait and only takes effect in
     landscape, where iOS reserves that strip for the rounded corner. */
  top: calc(40px + env(safe-area-inset-top, 0px));
  left: calc(10px + env(safe-area-inset-left, 0px));
  z-index: 210;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.5rem 0.85rem 0.5rem 0.6rem;
  /* slightly higher opacity than the cream-background default: this now
     also has to hold up over the busy HERO photo, not just flat --bg */
  background: rgba(251, 243, 228, 0.92);
  backdrop-filter: blur(10px);
  border: 1px solid var(--border);
  border-radius: 999px;
  box-shadow: 0 4px 16px rgba(20, 12, 4, 0.18);
  transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
/* PATCH 007.3 — the visible pill is 89 x 38px, which is under the comfortable
   minimum for a thumb. This extends the hit area to roughly 101 x 52px without
   changing a single pixel of the control: it is transparent, it lives inside
   the button so a press on it is a press on the button, and it sits over the
   HERO photo, which has no interactive content of its own.
   It hangs off the LABEL rather than off .menu-trigger::after, where it used
   to live, so that both of the button's own pseudo-elements are free for the
   perimeter wave further down. The geometry is unchanged, not merely similar:
   .menu-trigger__label is not positioned, so an absolutely positioned
   pseudo-element of it resolves against the same containing block the old
   rule used — .menu-trigger's padding box — and the same inset therefore
   describes the same rectangle. Hit-testing still lands on an element inside
   the button, so a press is still a press on the button. */
.menu-trigger__label::after {
  content: "";
  position: absolute;
  inset: -7px -6px;
}
.menu-trigger:hover, .menu-trigger:focus-visible {
  border-color: var(--border-strong);
  box-shadow: var(--card-shadow-hover);
}
.menu-trigger:focus-visible {
  outline: 2px solid var(--honey-text-deep);
  outline-offset: 2px;
  /* DUAL RING: this control sits on the HERO photograph, where a single
     colour cannot hold 3:1 -- a deep ring disappears into a dark frame, a
     light one into a bright frame. The --surface band fills the 2px offset
     gap the outline already reserved, so the indicator stays exactly 4px
     deep and nothing moves; the two bands contrast 5.34:1 with each other,
     which is what remains readable when neither clears the photo itself.
     --card-shadow-hover is restated because this declaration would
     otherwise replace the one on the :hover/:focus-visible rule above. */
  box-shadow: 0 0 0 4px var(--surface), var(--card-shadow-hover);
}
.menu-trigger__icon {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 4.5px;
  width: 18px;
}
/* PATCH 007.3 — ONE ICON, TWO STATES.
   It used to be two strokes of different lengths (16px over a 10px one, the
   short one right-aligned), which read as a mark rather than a menu control,
   and the X was made by re-widening both to 14px -- a different stroke length
   in each state. Now it is a proper hamburger: THREE bars of exactly the same
   length, thickness and corner treatment, evenly spaced. The middle bar is the
   icon's ::before rather than new markup, so index.html is untouched and the
   wrapper's aria-hidden still covers the whole icon; `order` puts it between
   the two existing spans.

   Opening rotates the SAME top and bottom bars into the X and fades only the
   middle one out. Their length never changes, so the X is drawn with the exact
   stroke system of the hamburger -- not shorter, not thicker, not lighter. The
   bar pitch is 1.5 + 4.5 = 6px, which is why each one travels exactly 6px to
   meet the other at the icon's centre; the X is therefore geometrically
   centred in the same box. 16px -> 18px is one modest step up (+12.5 %) with
   the stroke weight left at 1.5px, so the icon gains presence without turning
   chunky, and the taller icon still sits inside the label's line box, so the
   control's own height -- and the layout around it -- does not change. */
.menu-trigger__icon span,
.menu-trigger__icon::before {
  display: block;
  width: 18px;
  height: 1.5px;
  background: var(--wood-deep);
  transition: transform 0.25s ease, opacity 0.25s ease;
}
.menu-trigger__icon::before { content: ""; order: 2; }
.menu-trigger__icon span:first-child { order: 1; }
.menu-trigger__icon span:last-child { order: 3; }
.menu-trigger[aria-expanded="true"] .menu-trigger__icon span:first-child { transform: translateY(6px) rotate(45deg); }
.menu-trigger[aria-expanded="true"] .menu-trigger__icon span:last-child { transform: translateY(-6px) rotate(-45deg); }
.menu-trigger[aria-expanded="true"] .menu-trigger__icon::before { opacity: 0; }
.menu-trigger__label {
  font-family: var(--font-label);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  color: var(--wood-deep);
  /* the word itself is swapped by js/main.js at the midpoint of this fade,
     so MENU and CLOSE cross over rather than snapping */
  transition: opacity 0.16s ease, transform 0.16s ease;
}
.menu-trigger.is-label-swapping .menu-trigger__label {
  opacity: 0;
  transform: translateY(2px);
}

/* ---------- OPEN STATE: this control IS the close control ----------
   The upper-right .menu-overlay__close is gone, so the trigger has to read
   as a close affordance on its own. It takes over that control's GLOBAL
   CLOSE UI spec token for token -- solid --honey-text-deep fill, --surface
   text, the same rgba(232,169,59,0.45) glow, the same #7c4a08 pressed tone,
   the same two-ring @keyframes closeUIRipple (declared with the overlay
   below and still shared with .career-archive__close) -- so nothing new
   enters the palette and the two close contexts still speak one language.
   Dimensions are NOT copied: the pill keeps its own padding, radius, font
   and icon, because the upper-left control is an icon+label pill, not a
   text-only tab.
   Every declaration in this block is behind [aria-expanded="true"], which
   js/main.js already maintained and which already drives the hamburger -> X
   morph above. The closed state is therefore untouched, and the morph rules
   are untouched -- only the colours they paint in change. */
.menu-trigger[aria-expanded="true"] {
  background: var(--honey-text-deep);
  border-color: var(--honey-text-deep);
  box-shadow: 0 0 16px 2px rgba(232, 169, 59, 0.45);
}
.menu-trigger[aria-expanded="true"] .menu-trigger__label { color: var(--surface); }
.menu-trigger[aria-expanded="true"] .menu-trigger__icon span,
.menu-trigger[aria-expanded="true"] .menu-trigger__icon::before { background: var(--surface); }
.menu-trigger[aria-expanded="true"]:hover {
  background: #7c4a08;
  border-color: #7c4a08;
  box-shadow: 0 0 20px 3px rgba(232, 169, 59, 0.6);
}
/* The dual ring accepted in the focus batches is kept exactly as it is: 4px
   of --surface filling the 2px gap the outline's own offset reserves. It is
   only gaining a better ground -- --surface against the --honey-text-deep
   fill is 5.34:1 -- and the trailing shadow is swapped for the open state's
   glow so the indicator is never lost inside the wave. */
.menu-trigger[aria-expanded="true"]:focus-visible {
  background: #7c4a08;
  border-color: #7c4a08;
  box-shadow: 0 0 0 4px var(--surface), 0 0 20px 3px rgba(232, 169, 59, 0.6);
}

/* ---------- LIGHT WAVE: THE WHOLE PILL IS THE EMITTER ----------
   The wave used to start as a 30px circle centred on the icon, so it read as
   a ripple coming off the X. It does not: the emitter is the complete control,
   and what leaves it is its own silhouette. Both rings are now pseudo-elements
   of .menu-trigger itself, sized by inset rather than by a width/height, so the
   contour IS the button box -- it follows the pill from MENU to CLOSE and at
   every viewport with nothing hard-coded and nothing to keep in sync. The icon
   no longer carries a ring, and its position:relative -- added only to anchor
   that ring -- is gone with it.
   border-radius: inherit takes the button's own 999px, so a uniform scale()
   keeps a capsule a capsule: left and right travel together, top and bottom
   travel together, and the silhouette never rounds off into a circle.
   inset:-2px starts the contour 1px clear of the pill's border, so the wave
   appears just OUTSIDE the button rather than repainting its edge.
   scale() and opacity only, per the site's motion rule, and scale is also what
   keeps this self-sizing. 1.15 is the ceiling, not a taste call: the pill sits
   at left:10px and is 97.83px wide at every viewport, so the contour's centre
   is 58.9px from the screen edge and anything past 1.157 would push its left
   side off-screen at the widest, faintest moment. 1.15 gives about 7.6px of
   travel sideways and 3.2px top and bottom -- comparable to the ring it
   replaces, felt rather than announced, never clipped and never reaching the
   navigation.
   Both sit at z-index -1. .menu-trigger establishes a stacking context
   (backdrop-filter), so that paints them under the bars and under the word
   CLOSE, which can never be obscured, while the part that has travelled past
   the pill still paints over the overlay below. */
@keyframes menuPerimeterWave {
  0%   { transform: scale(1);    opacity: 0.6; }
  /* the fall is front-loaded: brightest against the button, then softening
     fast, so the contour reads as light leaving rather than as a badge */
  45%  { opacity: 0.18; }
  100% { transform: scale(1.15); opacity: 0; }
}
.menu-trigger[aria-expanded="true"]::before,
.menu-trigger[aria-expanded="true"]::after {
  content: "";
  position: absolute;
  inset: -2px;
  border: 1px solid var(--honey-500);
  border-radius: inherit;
  opacity: 0;
  z-index: -1;
  pointer-events: none;
  animation: menuPerimeterWave 3s ease-out infinite;
}
/* the second wave is the same contour half a cycle behind, which is what makes
   the emission continuous rather than a repeating single pulse */
.menu-trigger[aria-expanded="true"]::after { animation-delay: 1.5s; }

.menu-scrim {
  position: fixed;
  inset: 0;
  z-index: 195;
  background: rgba(58, 46, 32, 0.28);
  backdrop-filter: blur(2px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.menu-scrim.is-visible { opacity: 1; pointer-events: auto; }

.menu-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2rem;
  padding: 6rem 1.5rem 4rem;
  background: rgba(251, 243, 228, 0.94);
  backdrop-filter: blur(14px);
  opacity: 0;
  transform: translateY(-8px);
  pointer-events: none;
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.menu-overlay:not([hidden]) { display: flex; }
/* KEYBOARD TRAP FIX: the base .menu-overlay rule above sets display:flex
   unconditionally, which — being an author-stylesheet declaration — beats
   the UA stylesheet's [hidden]{display:none} regardless of source order or
   specificity (author origin always wins over UA origin for normal
   declarations). Closed (hidden) overlay content stayed in the tab order
   forever as a result. This rule only ever applies while [hidden] is
   present (mutually exclusive with :not([hidden]) above), and js/main.js
   already only sets hidden=true after its own 320ms close transition
   finishes, so this changes nothing about the open/close timing itself. */
.menu-overlay[hidden] { display: none; }
.menu-overlay.is-open {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.menu-overlay::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(198, 124, 26, 0.06) 1px, transparent 1px),
    linear-gradient(90deg, rgba(198, 124, 26, 0.06) 1px, transparent 1px);
  background-size: 48px 48px;
  pointer-events: none;
}
.menu-overlay__deco {
  position: absolute;
  top: 6rem;
  right: 6%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  color: var(--honey-500);
  opacity: 0.55;
}
.menu-overlay__line {
  width: 1px;
  height: 90px;
  background: linear-gradient(180deg, var(--honey-500), transparent);
}
.menu-overlay__nav { position: relative; z-index: 1; }
/* EDITORIAL LEFT ALIGN (preview) -- the <ol> stays exactly where it was:
   .menu-overlay's own align-items:center still centers this whole block as
   a unit, and no width/position changed here. Only the row's OWN main-axis
   packing moved from center to flex-start, so every row's content starts at
   the row's own left edge instead of floating mid-row. Because .menu-overlay
   __nav ol has no align-items of its own, its default (stretch) already made
   every <li>/<a> the same width -- the width of the longest row (PROFILE) --
   so this one change is also what gives 01/02/.../06 and TOP/PROFILE/... a
   shared vertical line: .menu-overlay__num's own min-width:2ch (below) does
   the rest, since every number is two digits. */
.menu-overlay__nav ol {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  text-align: left;
  list-style: none;
  margin: 0;
  padding: 0;
}
.menu-overlay__nav a {
  display: flex;
  align-items: baseline;
  justify-content: flex-start;
  gap: 0.9rem;
  padding: 0.55rem 0.5rem;
  font-family: var(--font-en-display);
  font-size: clamp(1.6rem, 5vw, 2.4rem);
  font-weight: 600;
  color: var(--ink);
  transition: color 0.2s ease;
}
.menu-overlay__nav a:hover, .menu-overlay__nav a:focus-visible, .menu-overlay__nav a.is-active { color: var(--honey-text); }
/* D-2: the row's two secondary elements both move to --ink-dim. Measured against
   the ground this overlay actually paints -- not a flat --bg -- they were 2.46
   (--honey-600) and 2.44 (--ink-faint) at the worst rendered pixel, because the
   0.94 overlay lets the HERO photo through and ::before adds the honey grid on
   top. No honey token is dark enough to clear AA there (--honey-text-deep still
   only reaches 4.00), so the numerals give up the gold; the overlay's opacity,
   blur, grid and structure are untouched, and MENU keeps its honey through
   __deco/__line, the grid itself, __close and the nav hover colour.
   --ink-dim also keeps the widest lightness gap to the --ink nav label of the
   candidates that pass, so 01-06 and the JP labels still read as secondary. */
.menu-overlay__num {
  font-family: "SF Mono", "Zen Kaku Gothic New", monospace;
  font-size: 0.85rem;
  letter-spacing: 0.05em;
  color: var(--ink-dim);
  min-width: 2ch;
}
.menu-overlay__jp {
  font-family: var(--font-jp-body);
  font-size: 0.65rem;
  letter-spacing: 0.15em;
  color: var(--ink-dim);
  align-self: center;
}
/* GLOBAL CLOSE UI — the shared ripple, now used by .career-archive__close
   (WORKS modal close, further down in this file) alone. The MENU/CLOSE control
   up in the trigger block keeps the same colour and the same 3s two-ring
   rhythm but emits from its own pill perimeter, which needs a different
   travel, so it has its own @keyframes menuPerimeterWave there.
   .menu-overlay__close, the overlay's own upper-right CLOSE, was the third
   consumer and is deleted with its markup: the trigger now performs that
   action, and two equally strong close affordances is exactly what this
   change removes. The keyframes stay -- they were declared here and
   .career-archive__close still uses them. */
@keyframes closeUIRipple {
  0% { transform: scale(1); opacity: 0.5; }
  100% { transform: scale(1.6); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .menu-overlay, .menu-scrim,
  .menu-trigger__icon span, .menu-trigger__icon::before,
  .menu-trigger__label { transition: none; }
  /* the wave stops, but the OPEN control must stay obviously active without
     it: one ring is held at menuPerimeterWave's own 0% frame -- the complete
     pill perimeter, same colour, same place, simply not moving, never a circle
     around the X -- and the second is dropped, since two identical static
     contours would just read as a thicker one. */
  .menu-trigger[aria-expanded="true"]::before,
  .menu-trigger[aria-expanded="true"]::after { animation: none; }
  .menu-trigger[aria-expanded="true"]::after { opacity: 0; }
  .menu-trigger[aria-expanded="true"]::before { opacity: 0.5; }
}

/* =========================================================
   D: BEE SYMBOL — flat black x honey-gold, reusable at any size
   ========================================================= */
.bee-icon {
  width: 1.4em;
  height: 1.05em;
  flex-shrink: 0;
  vertical-align: middle;
}
.bee-wing { fill: var(--honey-300); fill-opacity: 0.65; stroke: var(--honey-600); stroke-width: 1; }
.bee-body { fill: var(--ink); }
.bee-stripe { fill: var(--honey-500); }
.bee-head { fill: var(--ink); }
.bee-antenna { stroke: var(--ink); stroke-width: 1.6; fill: none; stroke-linecap: round; }

/* =========================================================
   A: FOREST LAYER — bottom-edge-only crop, never a hero frame
   ========================================================= */
.section--forest {
  position: relative;
  overflow: hidden;
}
.section--forest::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 260px;
  max-height: 34%;
  background-image: url("../assets/graphics/forest-layer-a.webp");
  background-repeat: no-repeat;
  background-position: center bottom;
  background-size: min(1100px, 130%) auto;
  opacity: 0.32;
  z-index: 0;
  pointer-events: none;
}
.section--forest > * { position: relative; z-index: 1; }
@media (max-width: 640px) {
  .section--forest::before { height: 160px; opacity: 0.26; }
}
/* quieter variant: PROFILE and CONTACT bookend the page ("静") so the
   forest reads as a hint, not a backdrop; WORKS drops the class entirely
   so photos carry the section instead of competing background art */
.section--forest-soft::before { opacity: 0.16; }
@media (max-width: 640px) {
  .section--forest-soft::before { opacity: 0.13; }
}

/* =========================================================
   SECTION BACKGROUND PROTOTYPE 01 — Mobile 390 only (OPEN, NOT FINAL)
   Each section gets its own approved decorative "world" instead of one
   thread tying every section together — HERO storybook / PROFILE paper /
   NEWS honeycomb / WORKS wood grain / SNS game graphic / CONTACT forest
   world. Desktop is completely untouched (this
   whole block is inside max-width:759px, matching the existing CONTACT
   Mobile breakpoint). Every layer sits behind real content (z-index 0,
   content itself lifted to z-index 1 where a section didn't already do
   that) and is pointer-events:none — never blocks a tap, a link, or the
   CONTACT form, and never touches GLOBAL HONEY, MENU, WORKS' own photo-
   switch logic, or any copy/layout. Assets used as supplied — no color/
   filter edits beyond opacity — under assets/section-bg/ (originals
   preserved separately under assets/section-bg/original/).
   ========================================================= */
.contact__section-bg { display: none; } /* Desktop-safe default; display:block only inside the mobile query below */

@media (max-width: 759px) {
  /* HERO — 03 STORYBOOK TEXTURE. .hero__photo is a full-bleed 100svh
     photo with no gaps, so this sits behind it and is intentionally
     near-invisible during HERO itself (photo/typography/HONEY stay the
     leads, per spec) — see the report for why it can't be made more
     visible without touching the (frozen) photo layout. */
  #hero::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background-image: url("../assets/section-bg/section-bg-03-storybook.webp");
    background-size: cover;
    background-position: center;
    opacity: 0.4;
    pointer-events: none;
  }

  /* PROFILE — 01 PAPER TEXTURE (subtle: fiber/mottling perceptible, not
     "paper is sitting here"). Uses ::after since ::before is already the
     FROZEN forest-layer decoration (.section--forest) — both coexist,
     paper as the quiet base under the existing forest accent. */
  #profile::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background-image: url("../assets/section-bg/section-bg-01-paper.webp");
    background-size: cover;
    background-position: center;
    opacity: 0.16;
    pointer-events: none;
  }

  /* NEWS — 02 HONEYCOMB A (medium/slightly strong — hexagons must read,
     text must not be fought). 04 HONEY TRACE is prepared but left OFF
     this round per spec §3 ("最初から強く重ねないでください... 必要性が
     なければPROTOTYPE 01ではOFFでも構いません"). */
  #news::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background-image: url("../assets/section-bg/section-bg-02-honeycomb.webp");
    background-size: cover;
    background-position: center;
    opacity: 0.4;
    pointer-events: none;
  }
  #news > * { position: relative; z-index: 1; }

  /* WORKS — 05 WOOD GRAIN (medium — a material change is felt on entry,
     but the showcase photos stay the lead). */
  #works::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background-image: url("../assets/section-bg/section-bg-05-wood-grain.webp");
    background-size: cover;
    background-position: center;
    opacity: 0.3;
    pointer-events: none;
  }
  #works > * { position: relative; z-index: 1; }

  /* SNS — 06 DIGITAL GAME WORLD v2 (DIGITAL GAME WORLD v2 + LOOP SCROLL
     DELTA). Replaces the previous GAME GRAPHIC asset, whose HUD glyphs
     all lived in its border around a blank center; this one scatters
     game elements (HUD, pixel hearts, grid, route lines, SELECT/EXP/
     SAVE POINT/LEVEL UP/STAGE SNS) across the whole frame, AND carries
     a full-width scalloped band at both the very top and very bottom —
     bands that survive any horizontal crop regardless of
     background-position, since a tall/narrow section only crops this
     landscape image horizontally (cover is height-constrained), never
     vertically. background-position:center is enough for "still
     clearly a game world" to survive cropping; opacity raised from the
     previous asset's 0.5 (permitted — SNS logos still stay the lead,
     this is z-index 0 under content). */
  #sns::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background-image: url("../assets/section-bg/section-bg-06-game-graphic-v2.webp");
    background-size: cover;
    background-position: center;
    opacity: 0.58;
    pointer-events: none;
  }
  #sns > * { position: relative; z-index: 1; }

  /* CONTACT — 07 FOREST WORLD (medium/strong — greens allowed to read
     clearly here, per spec §3). A new layer, not ::before/::after, since
     .contact--system already uses both for the existing forest-edge
     traces (bottom + top-right mirrored) — this sits behind all of that
     at z-index 0, via .contact__section-bg in index.html. (It used to
     share that z-index with the SYSTEM sysbg/datarain layers; those
     were retired in U1-C and no longer exist.)
     background-position:center lines the asset's own bright clearing up
     roughly behind the form column, per spec's "center brightness behind
     the form" guidance — exact alignment depends on scroll position since
     the section is taller than one viewport. */
  .contact__section-bg {
    display: block;
    position: absolute;
    inset: 0;
    z-index: 0;
    background-image: url("../assets/section-bg/section-bg-07-forest-world.webp");
    background-size: cover;
    background-position: center;
    opacity: 0.55;
    pointer-events: none;
  }

  /* CONTACT — LONG SECTION FOREST RHYTHM (POLISH DELTA 01 §4). The base
     layer above is untouched (top canopy / bright center behind the form
     stay exactly as approved). This adds a second use of the SAME asset,
     confined to a short band pinned to the very bottom of the section via
     ::after (a free slot — .contact--system's own ::before/::after are
     already the existing forest-edge traces), so the section doesn't fade
     into plain paper by the time a visitor scrolls past the form: cropped
     to the asset's own bottom ~40% (moss / fern / mushrooms / roots),
     near-full width, so the section resolves into visible forest floor
     rather than empty card at its end. No new asset, no change to the
     base layer's look. */
  .contact__section-bg::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 150px;
    background-image: url("../assets/section-bg/section-bg-07-forest-world.webp");
    background-size: 620px auto;
    background-position: center bottom;
    background-repeat: no-repeat;
    opacity: 0.55;
    pointer-events: none;
  }

  /* SECTION DIVIDER — PROTOTYPE (ADDITIONAL DELTA §2/§3). A thin honey-
     gold rule at PROFILE→NEWS, NEWS→WORKS, WORKS→SNS, SNS→CONTACT (not
     HERO→PROFILE, per spec). border-image instead of a flat 1px solid
     border so it isn't a plain digital line — the repeating dash/gap
     pattern (irregular 5/2/6/2px segments, alternating full/faint tone)
     reads closer to an inked, slightly uneven storybook rule. No new
     asset, no animation, no glow, no gradient fade at the ends. Sits in
     each section's own top padding (.section has 6rem top padding), so
     it never touches real content. */
  #news, #works, #sns, #contact {
    border-top: 2px solid transparent;
    border-image: repeating-linear-gradient(
      90deg,
      rgba(198, 124, 26, 0.8) 0px, rgba(198, 124, 26, 0.8) 5px,
      rgba(198, 124, 26, 0.3) 5px, rgba(198, 124, 26, 0.3) 7px,
      rgba(198, 124, 26, 0.8) 7px, rgba(198, 124, 26, 0.8) 13px,
      rgba(198, 124, 26, 0.3) 13px, rgba(198, 124, 26, 0.3) 15px
    ) 1;
  }
}

/* ---- DEFERRED SECTION BACKGROUNDS (PERF P1-A) ----
   A background-image is requested the moment its rule applies to a rendered
   box, wherever that box sits: measured cold at 390, the NEWS honeycomb,
   the WORKS wood grain and the CONTACT forest world above (666 KB, three
   to seven viewports below the fold) were all in flight before the visitor
   had scrolled. So, once the first inline script in index.html has set
   html.has-deferred-bg, each of those layers stays image-less until
   js/main.js marks its element .is-bg-ready -- which it does only after
   the file has actually arrived (an Image() probe for the same URL, so the
   layer paints from cache in one step -- never a none-to-image flash; on a
   slow link the whole layer can still arrive after the section does).
   Everything else about the layers -- size, position, opacity, stacking --
   is the approved rule above, untouched; this only withholds the image.
   Without the script the class is never set, the gate never applies and
   the three backgrounds load exactly as before. Desktop (>=760px) has no
   section backgrounds, so nothing here can apply there. */
html.has-deferred-bg #news:not(.is-bg-ready)::before,
html.has-deferred-bg #works:not(.is-bg-ready)::before,
html.has-deferred-bg .contact__section-bg:not(.is-bg-ready),
html.has-deferred-bg .contact__section-bg:not(.is-bg-ready)::after {
  background-image: none;
}

/* =========================================================
   SECTION SHELL / TITLES
   ========================================================= */
main { padding-top: 0; }

.section {
  max-width: var(--max-w);
  margin: 0 auto;
  /* POST-LAUNCH VISUAL PATCH 001: 6rem -> 4.5rem (Mobile) / 5rem (>=760).
     The section eyebrows (02-06) are gone, so the boundary no longer needs
     the ceremonial 96px on each side of the divider; the two values keep
     Desktop a touch more generous, as the design already does for CONTACT.
     Horizontal 1.5rem is untouched. HERO does not use .section and PROFILE
     overrides only its own top (HERO seam), so this moves exactly the
     PROFILE-bottom / NEWS / WORKS / SNS edges. */
  padding: 4.5rem 1.5rem;
  position: relative;
}
@media (min-width: 760px) {
  .section { padding: 5rem 1.5rem; }
}
/* PATCH 001 DELTA 02: NEWS / WORKS / SNS wait less before their heading.
   Top edge only -- the shared 4.5rem / 5rem stays the baseline, each
   section's bottom keeps it, and PROFILE / QUALIFICATIONS / CONTACT are
   untouched. The three ids are the same set the Mobile divider rule uses. */
#news, #works, #sns { padding-top: 3.5rem; }
@media (min-width: 760px) {
  #news, #works, #sns { padding-top: 4rem; }
}
.section--soft { background: var(--bg-soft); }

.section__head {
  margin-bottom: 3rem;
  text-align: center;
}
.section__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-family: var(--font-label);
  font-size: 0.72rem;
  letter-spacing: 0.24em;
  color: var(--honey-text);
  text-transform: uppercase;
}
/* UI = modern/digital accent: a thin gradient rule under every main section
   heading, contrasting with the organic graphic layer behind it.
   PATCH 001: the rule used to hang off .section__eyebrow::after, but it was
   always positioned against .section__head (the eyebrow itself is not
   positioned), so moving the pseudo-element onto the head keeps the line at
   exactly the same place -- 0.6rem under the JP subtitle -- with the same
   colour, width and thickness, now that the eyebrows are gone. The CAREER
   ARCHIVE head is excluded on purpose: its eyebrow is removed with the
   02-06 numbering and it never showed the rule on Mobile. */
.section__head { position: relative; }
.section__head:not(.career-archive__head)::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -0.6rem;
  transform: translateX(-50%);
  width: 40px;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--honey-500), transparent);
}
/* small monospace section counter (e.g. "02 / 06") — the digital/UI
   register set deliberately against the organic eyebrow label beside it */
.section__num {
  font-family: "SF Mono", "Zen Kaku Gothic New", monospace;
  font-size: 0.66rem;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
  margin-right: 0.6em;
}
/* editorial variant: breaks the "everything is centered" rhythm for
   sections where an asymmetric composition is intentional (PROFILE) */
.section__head--left {
  text-align: left;
  margin-left: 0;
}
.section__head--left:not(.career-archive__head)::after { left: 0; transform: none; }
.section__title {
  font-family: var(--font-en-display);
  font-size: clamp(1.9rem, 4.5vw, 2.7rem);
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--ink);
  margin-top: 0.5rem;
}
.section__title-jp {
  display: block;
  font-family: var(--font-jp-display);
  font-size: 0.9rem;
  letter-spacing: 0.3em;
  color: var(--ink-dim);
  margin-top: 0.6rem;
}

/* subtle leaf / hex confetti background, used behind sections */
.leaf-decor {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}
.leaf-decor svg { position: absolute; opacity: 0.5; }

/* =========================================================
   HERO
   ========================================================= */
/* v2.2: designed around this one photo, not a generic hero template.
   The image is the stage — no card, no frame, no border. Numbers below
   (object-position, overlay height) are tuned against the specific
   crop of assets/photos/hero-portrait.jpg: face+arms upper-left/center,
   table + mini cars along the bottom edge, blurred bar shelf upper-right
   (the negative space the name sits in, roughly on his sightline). */
.hero { position: relative; }

.hero__photo {
  position: relative;
  width: 100%;
  height: 100svh;
  min-height: 560px;
  overflow: hidden;
}
/* PATCH 007.4 -- the cinematic zoom's carrier. An in-flow block that occupies
   exactly the box the image used to occupy on its own, so the image inside it
   is laid out and cover-fitted against an identical W x H and paints in the
   same step of .hero__photo's paint order (non-positioned in-flow content,
   below .hero__photo::after, .hero__type and .scroll-cue, and below the mobile
   #hero::before texture). Nothing here is positioned, promoted or clipped: the
   clip is still .hero__photo's own overflow:hidden. */
.hero__zoom-layer {
  display: block;
  width: 100%;
  height: 100%;
}
.hero__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* PC: at the 1440x900 benchmark viewport this crops only ~6% off the
     top/bottom, so the full composition (face, arms, table, all 5 cars)
     stays in frame; nudged up slightly so the cars keep clear margin
     from the bottom edge. */
  object-position: 50% 38%;
  display: block;
}
/* soft cream bleed at the foot of the photo so PROFILE's ivory ground
   feels like it arrives rather than cuts in — no wave, no curve */
.hero__photo::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 16%;
  /* v2.2.1: kept short and mostly transparent so the mini cars read
     crisply — only the last sliver actually blends into cream */
  background: linear-gradient(to bottom, transparent 45%, var(--bg) 96%);
  pointer-events: none;
}

.hero__type {
  position: absolute;
  top: 17%;
  right: 6%;
  z-index: 1;
  text-align: right;
  max-width: 60%;
}
.hero__genre {
  font-family: var(--font-mono);
  font-size: clamp(0.62rem, 1.1vw, 0.78rem);
  letter-spacing: 0.08em;
  color: #fffdf8;
  /* READABILITY PASS: the old shadow was a warm brown (30,20,10) at .3 over
     6px, which tinted more than it separated. This is a neutral black over
     a tighter 4px blur, same 1px offset -- still a shadow, never an outline
     or a glow. Nothing else about the line changes: size, letter-spacing,
     line-height, colour and wording are all untouched.

     Alpha is .5, not the .35 the direction offered as a starting point.
     Measured at a fixed position with only this property varying (so the
     backdrop is identical), the mean luminance of the 1px ring around the
     glyphs moves from the old shadow by: .35 -> -1.1 (1440) / -1.3 (390)
     levels, .45 -> -2.0 / -2.4, .55 -> -3.1 / -3.5, .65 -> -4.1 / -4.6.
     At .35 the change is about one level out of 255 -- below perception,
     i.e. no readability gain at all over what was already there. .5 lands
     around -2.7 levels: a real but still quiet lift. The backdrop behind
     this line is already dark (luminance ~45-52), which is why even .65
     only buys ~4.5 levels; a stronger effect would need a bigger blur or a
     second layer, and both were ruled out as glow/outline. */
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
  margin-bottom: 0.6rem;
}
/* DESKTOP-ONLY OPTICAL ALIGNMENT for the genre line.
   .hero__type is position:absolute, so it is shrink-to-fit: its width is
   set by its widest child, which is the AYAGIRI / TAKUYA block. The genre
   line inherits text-align:right from .hero__type and therefore hangs off
   the same right edge as the name, which reads as hanging off the end of
   AYAGIRI rather than belonging to it. Centring it inside that same box
   puts it on the name block's own axis.

   Done with text-align rather than a measured offset on purpose: the shift
   this needs is (name width - genre width) / 2, and both depend on fonts
   that this environment cannot load (Google Fonts is unreachable in the
   sandbox), so any px or em number measured here would be wrong on a real
   device. text-align resolves against whatever the real fonts render, at
   every width, with no magic number to go stale.

   Mobile (<=640px, the HERO's own breakpoint) keeps the existing
   right-aligned position exactly -- 641px is the complement of the
   .hero__type mobile override below. The name itself never moves: it stays
   right-aligned against the same edge, and .hero__type's own box is
   unchanged because the genre is the narrower child either way. */
@media (min-width: 641px) {
  .hero__genre { text-align: center; }
}
.hero__genre-sep { margin: 0 0.5em; opacity: 0.6; }
.hero__name {
  font-family: var(--font-hero);
  font-weight: 600;
  /* v2.2.1: dialed back ~15% (from clamp 3/8.6/6.6rem) and the heavy
     drop-shadow replaced with a single soft one — legibility now comes
     mostly from sitting in the photo's negative space, not from the
     shadow doing the work. Reads editorial rather than thumbnail-text. */
  font-size: clamp(2.5rem, 7.3vw, 5.6rem);
  line-height: 0.96;
  letter-spacing: 0.01em;
  color: #fffdf8;
  text-shadow: 0 2px 12px rgba(20, 12, 4, 0.28);
}

/* D-5A/B: the SCROLL cue that sat at the photo's lower right is removed --
   markup, rules, its pulse keyframes and its reduced-motion override all
   went with it. It was --ink-faint text laid straight over the photograph,
   landing on the red mini car and the wood table edge, and its glyph area
   spanned near-black to near-white background: measured per glyph pixel,
   100% of it fell below AA at 320/390/430/1440 with a best pixel of 2.94.
   No flat colour could fix that -- against a ground spanning 0.09 to 0.94
   luminance, AA needs a text luminance above 4.4 or below zero, so light
   and dark text are both out. The cue was also redundant navigation: it
   pointed at #profile, which MENU already reaches, so nothing is lost but
   the instruction to scroll. */

@media (max-width: 640px) {
  .hero__photo {
    /* shorter than 100svh on purpose: cover-fit against this wide photo
       inside a full-height 390x844 box would leave only a ~30%-wide sliver
       (near face-only). Capping the height keeps a ~55-60% wide window,
       wide enough for face + both arms + three of the five cars. */
    /* MOBILE HERO HEIGHT — WIDTH-DRIVEN (PATCH 007.5, root-cause fix).
       This box used to be `clamp(380px, 58vh, 520px)`, and that 58vh was the
       defect. cover-fitting this photo is height-driven here (the box is 0.797
       wide-to-tall against the image's 1.5006, so the scale always comes off
       the height), which made the box height the single input deciding the
       visible crop. 007.4-H measured the consequence: a 97px viewport-height
       change -- the size of an in-app browser's chrome transition -- moved the
       crop from [156.11, 679.59] to [133.16, 767.85], shrinking the photo 11.5%
       and widening the visible window 13%, in every HERO state. The clamp gave
       no cover either: its vh-driven band runs from roughly 655px to 897px of
       viewport height, which is the whole range a real iPhone lives in.

       Safari never reproduced it and LINE's in-app browser did, which fits:
       since iOS 15.4 `vh` maps to the large viewport and does not move when
       Safari's own chrome collapses, but an in-app browser resizing its web
       view's frame recomputes every viewport unit. 007.4-I made the height a
       function of width instead, and the LINE defect went away on the device.

       So the shape here is the accepted rule with its middle term replaced:
       the same 380px floor, a 462.8px ceiling, and a width-derived term
       between them instead of 58vh. 007.5 first wrote the ratio as the
       then-accepted geometry exactly -- 390 wide by the 489.515625px that 58vh
       resolves to at the 390x844 reference -- reproducing that composition to
       the pixel, and because modern iPhones all sit near the same 0.462 aspect
       it landed within a pixel of what 58vh used to give on each of them:
       360x780 -0.5px, 390x844 0.0px, 393x852 -0.9px, 414x896 -0.0px, and
       430x932 exactly 520 either way.

       The MOBILE HERO DISTANCE refinement (OPTION D) then moved that geometry
       on purpose: the ratio and the ceiling are both 0.89x their 007.5 values
       -- 435.67 and 462.8 against 489.515625 and 520 -- pulling the mobile
       frame back so the photograph's own room, and not only its subject, is in
       shot. The factor is uniform, so the width-driven shape is untouched and
       every device simply sits that much lower than the line above: against
       58vh, 360x780 -50.2px, 390x844 -53.9px, 393x852 -55.1px, 414x896
       -57.2px, 430x932 -77.8px. The squarer iPhone SE (375x667) still moves
       the other way, now +32.1px rather than 007.5's +83.8px -- a width-driven
       rule cannot tell a 375x667 screen from a 360x780 one, and that is the
       cost of not reading the viewport height at all.

       max-height is what keeps the top of the range proportionate: without it
       the ratio would run to 714.9px at 640px wide. It binds from 414.29px of
       width up -- unchanged by the 0.89x, since scaling ratio and ceiling
       together leaves their crossover where it was. min-height binds below
       340.17px (302.75px before the refinement), so it now clamps the 320px
       class -- iPhone 5 / 5s / SE 1st generation -- to 380px, giving those
       screens a smaller pull-back than the rest; every current device is 375px
       or wider and is unaffected. Both are fixed px, so neither reintroduces a
       viewport-height dependency. No JS, no listeners, no captured height. */
    height: auto;
    aspect-ratio: 390 / 435.67;
    min-height: 380px;
    max-height: 462.8px;
  }
  .hero__img { object-position: 26% 50%; }
  /* pushed down from the very top edge for more breathing room against
     the photo (and the TEST BUILD banner above it); still clears the
     face, which starts around 21% of the photo's own height here */
  .hero__type { top: 14%; right: 5%; max-width: 66%; }
  .hero__name { font-size: clamp(2.1rem, 9.4vw, 2.9rem); }
}

/* ---------- HERO — CINEMATIC SLOW ZOOM OUT ----------
   BACKGROUND IMAGE MOTION ONLY. The photo settles from a slightly tighter
   frame to its approved one over the first 5.5s of the page, then stops.

   WHAT MOVES IS NOT THE IMAGE (PATCH 007.4). The animation is declared on
   .hero__zoom-layer, a plain <div> wrapping the <img>. The image element is
   never an animation target, at any point in the document's life: its
   computed animation-name is `none`, its transform is `none`, its
   transition-duration is 0s, and it is never promoted on its own account.

   Why that matters. 007.1 (retire the finished declaration) and 007.2 (take
   the photo out of hit testing) both left the <img> itself as the animated,
   accelerated element, and the real iPhone kept showing the photo destabilise
   -- fade-like re-entry and recompositing -- most reliably inside LINE / X
   in-app WebViews. An <img> that is the sole content of its own composited
   layer is exactly the case WebKit can serve as directly-composited image
   contents rather than as a painted backing store; the decoded bitmap then
   belongs to the layer, and anything that discards or re-supplies it (memory
   pressure, a re-parented or resized WKWebView, a rebuilt layer tree -- all
   routine in an in-app browser) is a contents swap on screen, not a repaint.
   A <div> is not a RenderImage and cannot take that path, so the moving layer
   is an ordinary painted one and the image underneath it is ordinary content.
   This is a structural argument, not a measured one: the failing engine
   cannot be run here, so it is confirmed only by real-device QA.

   Geometry is untouched by the move. The layer occupies exactly the box the
   image used to occupy, so object-fit/object-position resolve against an
   identical W x H, and transform-origin (50% 50% by default) is the centre of
   that same box -- the scaled result is the same pixels, just carried by the
   parent. .hero__type (name + genre), .scroll-cue and .hero__photo::after are
   SIBLINGS of the layer inside .hero__photo, not children of it, so none of
   them inherits this transform: the copy, the cue and the cream foot-bleed
   never move. GLOBAL HONEY and MENU are outside .hero entirely. Nothing about
   the approved crop changes --
   object-position, object-fit, the box height and the ::after bleed are
   all untouched, and the animation's END state is transform: scale(1),
   i.e. pixel-identical to the FROZEN HERO. Transform-origin is left at
   its 50% 50% default so the settle is symmetric and introduces no
   second crop basis alongside object-position.

   Runs ONCE: `1 normal backwards`. Never `infinite`, never `alternate`,
   never reversed into a zoom-in -- a loop or a reversal would pull the
   frame back off the approved crop, which is the one thing this must not
   do. `backwards` holds the start frame before the first paint, so there
   is no unscaled flash on load. transform never affects layout, and the
   scaled-up image is clipped by .hero__photo's existing overflow:hidden,
   so there is no layout shift and no horizontal overflow at any width.

   Why `backwards` and not `both`: `both` would leave `transform:
   matrix(1)` on the layer forever. That is the right SIZE, but it keeps
   the photo on a composited layer and rasterised through the compositor
   path -- measured against the frozen build, that shifts the whole photo
   by a mean of 1.27/255 (58% of pixels off by ~1, 0.13% off by more than
   8). Invisible, but it is not the approved render, and it holds a
   full-viewport GPU layer for the life of the page. With `backwards` the
   transform is simply gone once the animation ends, so the resting HERO
   is byte-identical to the FROZEN one (measured: 0 differing pixels
   outside .scroll-cue__line, whose own loop is unrelated) and the layer
   is released. Both fill modes end at the same SIZE, so dropping the
   transform is not a visual jump -- there is no geometric change at all
   at 5.5s, only the return to the ordinary paint path.

   Easing: cubic-bezier(0.36, 0.32, 0.48, 1). Read it as camera work -- a
   dolly that eases off its mark, runs at a readable rate through the
   first third, then decelerates the rest of the way and settles. Chosen
   against measurement, not by feel. Its initial slope is 0.32/0.36 =
   0.89 of the average rate, so the move is already visible in the first
   frames without a jerk (no sudden acceleration); its final control
   point sits at y=1, so the terminal velocity is zero (no hard cut). No
   control point leaves [0,1] on y, so the value never passes 1.00 and
   never comes back -- structurally no bounce and no overshoot.

   Measured at 1440, where the total travel is 68.6px per edge, in 0.5s
   steps: 6.4 / 8.1 / 9.6 / 10.2 / 9.7 / 8.3 / 6.5 / 4.6 / 3.0 / 1.7 /
   0.5px. One gentle ramp-in, a single peak around 1.5-2s, then a smooth
   continuous decay -- there is no second acceleration anywhere. Progress:
   21% at 1s, 50% at 2s, 76% at 3s, 93% at 4s, 99% at 5s.

   Amounts and duration per the v3 direction. v1 (12s from 1.075 / 1.05)
   and v2 (8s from 1.12 / 1.09) were both judged too hard to see on a real
   device; v3 is deliberately a step stronger -- 1.18 on Desktop, 1.14 at
   the HERO's own mobile breakpoint (<=640px), over 5.5s. The intent is a
   pull-back that is plainly visible as camera movement, while staying a
   slow settle rather than a web zoom effect. */
.hero__zoom-layer {
  --hero-zoom-from: 1.18;
  animation: heroZoomOut 5.5s cubic-bezier(0.36, 0.32, 0.48, 1) 1 normal backwards;
}
@media (max-width: 640px) {
  .hero__zoom-layer { --hero-zoom-from: 1.14; }
}
@keyframes heroZoomOut {
  from { transform: scale(var(--hero-zoom-from)); }
  to { transform: scale(1); }
}
/* ONE-SHOT (PATCH 007.1, retargeted by 007.4). The declaration above stays on
   .hero__zoom-layer for the life of the document, and a finished CSS animation
   whose declaration is still in place can be re-run by the engine when the
   element's rendering context is rebuilt -- a real-device report of the HERO
   re-entering after the entrance had already settled. With `backwards` a
   restart snaps the photo back to the start frame and replays the whole 5.5s
   settle, which is exactly what must never happen twice in one page session.
   007.4 moved what the latch guards from the image to the layer; it did not
   add a second completion system, and there is still exactly one.

   js/main.js retires the declaration by adding this class the moment the
   entrance's own animationend fires -- never before, so the settle is never
   cut short. At that point the animation is over and `backwards` is holding
   nothing, so removing it is not a visual change: the resting frame is the
   same transform-free, uncomposited paint it already was (measured: 0
   differing pixels). Nothing here plays a new animation, and a fresh page
   load starts without the class, so the entrance runs normally again. */
/* SETTLED HERO — 007.4-E's declaration, restored (PATCH 007.4-I).
   007.4-G replaced this with `will-change: transform` to hold the settled
   photo's compositor layer across an offscreen trip. It did exactly that in
   Chromium -- owned in 31/31 state probes where scale(1) owns none -- and the
   device still failed, in a pattern that contradicts the premise: the shallow
   returns where the layer is fully retained are the ones that almost always
   break, and the deep returns where it is genuinely released are the ones that
   often do not. 007.4-H then compared the browsers directly: Safari does not
   reproduce the defect at all, LINE's in-app browser does. That moves the cause
   off the compositor and onto the viewport, so the retention hint is removed
   here rather than left running alongside a second variable.

   `transform: scale(1)` comes back as the neutral baseline it was measured to
   be: 007.4-F found it creates no compositor layer in any frame, so it changes
   nothing about layers, costs no GPU memory, and leaves the settled frame the
   identity matrix it already resolved to. The point of restoring it is that the
   height experiment below is then the only new variable in the build.

   Unchanged from every earlier round: no translateZ or translate3d, no
   contain, no isolation, no backface-visibility, no perspective, no filter, no
   browser detection. The animation above is untouched, and reduced motion never
   reaches this rule -- the animation does not run there, so animationend never
   fires and is-hero-settled is never added. */
html.is-hero-settled .hero__zoom-layer { animation: none; transform: scale(1); }
/* THE HERO PHOTO IS NOT AN INTERACTION TARGET (PATCH 007.2, retained by 007.4
   as a secondary guard -- the architecture above is the primary fix now).
   007.1 made the entrance one-shot and left the settled image ordinary in
   every CSS sense (opacity 1, transform none, filter none, animation none,
   transition 0s, no will-change, no contain, no isolation) -- and the real
   device still showed the photo destabilising, but only while the visitor was
   touching and moving on it, never on its own. What was still special about it
   is not a style: it is that the <img> is the hit-test target for every touch
   on the first screen (measured: elementFromPoint at the HERO centre returns
   img.hero__img), and an <img> is a drag source. On iOS a press-and-move on an
   image starts the system drag lift, which fades the source out and back when
   the drag is refused, and it is also the callout and tap-highlight target.
   PATCH 006 suppresses the callout and the drop payload, but the lift begins
   before any dragstart is dispatched, so cancelling dragstart cannot stop the
   visual part.

   pointer-events removes the element from hit testing, so there is no image to
   lift, no callout target and no tap highlight: the touch lands on
   .hero__photo, an ordinary div with no handler, and the page scrolls exactly
   as before. Nothing is lost -- the photo has no hover, focus, click or JS
   handler anywhere, and .hero__type and the .scroll-cue link are its SIBLINGS,
   so both stay fully interactive. Image protection is strengthened rather than
   weakened: this photo can no longer be long-pressed at all. Layout, geometry,
   the entrance and every other section are untouched. */
.hero__img { pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  /* no motion at all: the approved frame from the very first paint */
  .hero__zoom-layer { animation: none; transform: none; }
}

/* HERO -> PROFILE seam only: the shared .section padding-top (6rem) left
   a long stretch of empty cream between the photo's fade and "02 / 06
   ABOUT". This shortens that one gap; PROFILE's own grid/photo/tags/copy
   are untouched. */
.profile { padding-top: 2.75rem; }
@media (max-width: 640px) {
  .profile { padding-top: 2.25rem; }
}

.tab-bar__btn:focus-visible {
  outline: 2px solid var(--honey-text-deep);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px rgba(232, 169, 59, 0.25);
}

/* =========================================================
   PLACEHOLDER BADGE (dummy photo marker)
   ========================================================= */
.placeholder-badge {
  font-family: var(--font-label);
  font-size: 0.66rem;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  text-align: center;
  padding: 0 1rem;
}

/* =========================================================
   PROFILE
   ========================================================= */
/* editorial, not symmetric: the photo runs larger and sits above its
   column baseline while the text column starts lower — a magazine-style
   offset rather than two centered, evenly-weighted halves */
.profile__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2.2rem;
  align-items: start;
  max-width: 920px;
  margin: 0 auto;
}
@media (min-width: 760px) {
  /* TABLET CLIPPING FIX: the photo column was a fixed 320px, so between
     760px and ~820px the text column had less room than
     .profile__statement-main's own min-content width — that line is
     white-space:nowrap (see its rule below: keeping the punchline on one
     line is the point of the section), so it overflowed its track and got
     clipped at the viewport edge, silently, because body has
     overflow-x:hidden. minmax lets the photo yield only the space the
     sentence actually needs: it still resolves to the full 320px from
     ~900px up (verified identical to the previous layout at 900/1024/
     1440), and never drops below the 200px floor. No breakpoint is
     hard-coded here on purpose — the threshold depends on the rendered
     width of that one sentence, which differs between the webfont and the
     fallback shown while it loads, and this shrinks only when and as much
     as the actual text requires under either. Typography is untouched. */
  .profile__grid { grid-template-columns: minmax(200px, 320px) 1fr; gap: 3.4rem; }
  /* DESKTOP VERTICAL BALANCE (PROFILE FINAL POLISH round): the previous
     3.2rem offset here was a deliberate "text column starts lower than
     the photo" editorial choice (see the file-level comment on
     .profile__grid above) — real-device review found it read as
     unbalanced rather than intentional, so the offset is retired.
     .profile__grid's own align-items:start already puts both columns'
     boxes at the same top; the name text's own line-height leaves a few
     px of natural leading above the kanji glyph, which is enough
     breathing room on its own for the two columns to read as starting
     from the same height without adding a second, explicit gap on top
     of it. Photo (.profile__photo-frame) is completely untouched. */
}
@media (max-width: 759px) {
  .profile__grid { gap: 1.5rem; }
}

.profile__photo-frame {
  position: relative;
  width: 100%;
  max-width: 320px;
  aspect-ratio: 3 / 4;
  margin: 0 auto;
  border-radius: 4px 24px 4px 24px;
  border: 1px solid var(--border);
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  object-fit: cover;
  object-position: center top;
}
@media (min-width: 760px) { .profile__photo-frame { margin: 0; } }

/* PHOTO GALLERY — the frame above (size/position/rounded corner)
   never changes; only what fills it does. RESPONSIVE PARITY (R1): the
   swipe gallery that was Mobile-only (<=759px) is now the one gallery
   at every viewport — same 3 photos, same order, same scroll-snap /
   loop-clone interaction, same dot indicator. Desktop previously showed
   only .profile__photo-slide--primary (the suit photo) as a static
   image; that class stays in the markup as js/main.js's "real first
   slide" anchor (initProfilePhotoGallery), it just no longer carries a
   display rule. Every declaration below is exactly the former Mobile
   value, so <=759px renders pixel-identical. */
.profile__photo-track {
  list-style: none;
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.profile__photo-track::-webkit-scrollbar { display: none; }
/* KEYBOARD ACCESS (R1): the track is a tab stop (tabindex="0" in
   index.html) so keyboard users can reach the gallery and move through
   the photos with the arrow keys — the same honey focus ring the SNS
   rail's links use, inset so the frame's rounded overflow:hidden corners
   never clip it. Pointer/touch users never see it (:focus-visible). */
.profile__photo-track:focus-visible {
  outline: 2px solid var(--honey-text-deep);
  outline-offset: -2px;
}
/* every slide (including both loop clones) is a full-frame, snap-aligned
   page; which one shows is purely a matter of scroll position. */
.profile__photo-slide {
  display: block;
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  scroll-snap-align: start;
}
.profile__photo-img { width: 100%; height: 100%; object-fit: cover; object-position: center top; }
/* PHOTO CROPPING — per-photo object-position (crop window only, never a
   resize/stretch: the frame's 3:4 aspect-ratio and object-fit:cover are
   shared by all three). Photo 01 (suit) keeps the center/top framing.
   Photo 02 (forest) is a full-body shot, center-cropped so head and feet
   both stay in frame. Photo 03 (food) is centered slightly lower so the
   spoon/bowl stays in the same frame as the face. */
.profile__photo-img[data-photo-id="forest"] { object-position: center 42%; }
.profile__photo-img[data-photo-id="food"] { object-position: center 30%; }

/* DOT INDICATOR — a quiet position marker, not a control: small,
   low-contrast when inactive, honey-gold only on the active photo,
   no pill/bar/box/number behind it. */
.profile__photo-dots {
  display: flex;
  position: absolute;
  left: 50%;
  bottom: 10px;
  transform: translateX(-50%);
  gap: 7px;
  z-index: 2;
}
.profile__photo-dot {
  width: 5px;
  height: 5px;
  border-radius: 999px;
  background: rgba(92, 69, 39, 0.55);
  /* a faint light ring so the dot still reads against a dark/busy
     photo, not just a flat-colored one — no pill/box behind it, just
     enough edge definition to stay legible over any of the 3 photos. */
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.5);
}
.profile__photo-dot.is-active {
  background: var(--honey-500);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.5);
}

.profile__name {
  font-family: var(--font-jp-display);
  font-size: 1.5rem;
  font-weight: 600;
  color: var(--ink);
}
.profile__name-en {
  display: block;
  font-family: var(--font-en-display);
  font-size: 0.85rem;
  letter-spacing: 0.14em;
  color: var(--ink-faint-text);
  margin-top: 0.4rem;
}

/* SNS QUICK LINKS — icon-only links beside the name. RESPONSIVE PARITY
   (R1): formerly Mobile 390 only (display:none elsewhere); now the same
   row at every viewport — same 5 official marks, same 36px tap boxes,
   same 19/23/22px optical sizes, same wood-brown monochrome filter, no
   enlargement on Desktop. Icons reuse the SNS section's own official
   marks/optical corrections (see SOCIAL_LOGOS in js/main.js) — no new
   assets, no labels, no description, no background. Every declaration
   here is the former Mobile value; the one Mobile-only rule left is the
   photo-frame alignment inset inside the max-width:759px query below. */
.profile__name-row {
  display: flex;
  align-items: flex-start;
  /* space-between: the name stays the row's first flex item (its own
     left position/font-size are untouched) and the icon group sits at
     the row's right edge — on Mobile that edge is pulled in to the photo
     frame's edge (padding-right below); on Desktop/tablet the row spans
     the text column, so the icons align with the body copy's right
     boundary instead. */
  justify-content: space-between;
}
/* OPTICAL POLISH 03 fix: as the icon group grows, flex's default
   min-width:auto let this shrink and wrap "AYAGIRI TAKUYA" onto two
   lines (its only break point is the single space) — name must
   never shrink/wrap, so the icon group is the only side that can
   ever give. */
.profile__name { flex-shrink: 0; }
.profile__quicklinks {
  display: flex;
  /* bottom-align the icon group so the marks' visual lower edge
     reads as one shared baseline (KEEP from the previous round). */
  align-items: flex-end;
  flex-shrink: 0;
  /* OPTICAL POLISH 02: raised from +0.3rem so the icon group's own
     visual center lines up with the kanji name's vertical center,
     not the gap between the kanji line and "AYAGIRI TAKUYA" below —
     measured against the JP text node's own rendered glyph box.
     .profile__name is 1.5rem at every viewport, so the same offset
     holds on Desktop. */
  margin-top: -3px;
}
/* TAP TARGET (unchanged since it was introduced): each icon's own
   visible size grows independently below; the tap box stays a
   larger invisible 36px box so the target itself never shrinks or
   grows with the visual icon size, and slightly negative side
   margins pull adjacent boxes back together so the row's total
   visual width stays reasonable — only the empty hit-area padding
   overlaps, never the icon marks themselves. OPTICAL POLISH 03:
   margins loosened to 0 (was -0.5px) for ~1px more breathing room
   now that Ameblo/LINE are visually larger, without touching the
   36px tap box. */
.profile__quicklink {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  width: 36px;
  height: 36px;
  margin: 0;
  opacity: 0.82;
}
.profile__quicklink:first-child { margin-left: 0; }
.profile__quicklink:last-child { margin-right: 0; }
.profile__quicklink-img {
  /* OPTICAL POLISH 02: 18px -> 19px */
  height: 19px;
  width: auto;
  display: block;
  /* desaturate first so every logo's own hue (X black, note green,
     YouTube red, Ameblo/LINE brand colors) starts from the same
     gray, then sepia+brightness tints all of them to the same
     wood-brown — a consistent monochrome mark, not five different
     tinted colors. */
  filter: grayscale(1) sepia(1) brightness(0.6) contrast(1.05);
}
/* OPTICAL POLISH 02 — X's source mark has ~0% internal padding (it
   fills its whole canvas edge-to-edge), while note/YouTube/Ameblo/
   LINE all carry ~4.5-4.8% padding on every side (measured from the
   source PNGs' real ink bounds, same technique as the SNS section's
   own X correction). At an identical box height that makes X's ink
   sit visibly lower/heavier than the other four, whose ink floats
   ~1px above their own image-box bottom edge — nudge X up to match. */
.profile__quicklink[data-icon="x"] .profile__quicklink-img { transform: translateY(-1px); }
/* ICON OPTICAL SIZE: Ameblo/LINE still read lighter than X/note/
   YouTube at the same box height (no change to X/note/YouTube's own
   19px above). OPTICAL POLISH 03: sized individually rather than as
   a pair — Ameblo needed the larger bump (21px -> 23px, ~+9.5%),
   LINE only a small one (21px -> 22px, ~+4.8%) — to close the
   remaining visual-weight gap without overshooting either one. */
.profile__quicklink[data-icon="ameba"] .profile__quicklink-img { height: 23px; }
.profile__quicklink[data-icon="line"] .profile__quicklink-img { height: 22px; }

@media (min-width: 760px) {
  /* DESKTOP ICON POSITION (PROFILE SOCIAL ICON LAYOUT CORRECTION): the
     base rule puts .profile__name and .profile__quicklinks on the SAME
     row with justify-content:space-between — on Mobile that's harmless
     (padding-right pulls the row's right edge in to the photo frame, a
     342px-wide row at most), but at >=760px the text column runs
     434-546px wide, so space-between stretched the gap between the name
     and its own icons out to 110-221px (measured), reading as a stray
     right-edge nav. A prior pass fixed that by stacking the icons under
     the two-line name block instead — moved too far the other way (user
     feedback: icons belong beside 綾切拓也, same line, not below AYAGIRI
     TAKUYA). Restored to the base row layout (no flex-direction
     override) with a fixed, modest gap replacing space-between, so the
     icons sit right after the name at a natural, constant distance
     instead of being pushed to either extreme. */
  .profile__name-row { justify-content: flex-start; gap: 1.25rem; }
  /* NAME EMPHASIS (PROFILE NAME EMPHASIS POLISH): 綾切拓也 is PROFILE's
     lead information — the enlarged Desktop icons above made the two
     read as similar visual weight. Fluid clamp(), continuous with the
     24px Mobile/base value at the 760px seam and settling at 29px
     (~1.21x, the smallest natural step inside the requested 1.15x-
     1.25x range) by 1440px, flat above. font-family/weight/color are
     untouched. .profile__name-en (AYAGIRI TAKUYA) is deliberately NOT
     scaled here, so the gap between it and 綾切拓也 widens too —
     sharpening the existing primary/secondary hierarchy rather than
     flattening it. */
  .profile__name { font-size: clamp(24px, 18.41px + 0.7353vw, 29px); }
  /* Icons vertically centered within their own (now taller, see the
     icon-scale clamps below) box rather than bottom-aligned — with
     .profile__name-row's own align-items:flex-start topping both items
     to the row's cross-start, centering here keeps the icon row's
     optical middle close to the kanji line's middle across the full
     36-46px box-height range, instead of one fixed offset that only
     held true at a single box size. */
  /* Small compensating nudge (margin, not transform) for the vertical
     drift the name-size clamp above introduces: the kanji line's own
     line-box grows with font-size (line-height 1.9 is inherited,
     untouched), so its optical center drifts down relative to the
     icon box's fixed-at-this-font-size center as 綾切拓也 grows past
     its 24px baseline. Fluid, tracking the same clamp curve (0px at
     760px where the name is still 24px, ~4.75px by 1440px where it
     reaches 29px), so centering stays correct at every width in
     between rather than only at the two endpoints. */
  .profile__quicklinks { align-items: center; margin-top: clamp(0px, -5.31px + 0.6985vw, 4.75px); }
  /* DESKTOP ICON SCALE — fluid clamp(), continuous with the Mobile
     value at the 760px seam (no jump) and settling at a larger, more
     legible size by 1440px (the project's desktop design baseline),
     flat above it. Every icon is scaled by the SAME ratio (19px->27px,
     x1.42) as its own Mobile height, so the existing per-icon optical
     corrections (Ameblo/LINE bumped up, X nudged) stay proportionally
     correct instead of being re-tuned by hand. The delivery PNGs are all
     96px wide (see SOCIAL_LOGOS in js/main.js; the 227-846px masters are
     retained unreferenced). The largest rendered requirement across
     320-2560px is 81px, on the YouTube mark at DPR3, so 96px leaves
     about x1.19 of headroom and this is a plain downscale at every width
     through DPR3. DPR4 is not covered: such a screen would upscale that
     mark roughly x1.12. No current mainstream device is DPR4. */
  .profile__quicklink-img { height: clamp(19px, 10.06px + 1.1765vw, 27px); }
  .profile__quicklink[data-icon="ameba"] .profile__quicklink-img { height: clamp(23px, 11.82px + 1.4706vw, 33px); }
  .profile__quicklink[data-icon="line"] .profile__quicklink-img { height: clamp(22px, 11.94px + 1.3235vw, 31px); }
  .profile__quicklink[data-icon="x"] .profile__quicklink-img { transform: translateY(-1.5px); }
  /* Tap box grows with the icon (36px -> 46px, same continuity/cap
     pattern) so the widest mark at full scale (YouTube, ~38px wide at
     1440) always sits inside it with room to spare — never a fixed
     36px box clipping a bigger icon. */
  .profile__quicklink { width: clamp(36px, 24.82px + 1.4706vw, 46px); height: clamp(36px, 24.82px + 1.4706vw, 46px); }
}

@media (max-width: 759px) {
  /* Mobile-only alignment inset (unchanged): mirrors .profile__photo-
     frame's own centering inset (the frame is max-width:320px, margin:0
     auto, inside this same-width single grid column) so the icon
     group's right edge lines up with the photo frame's right edge
     above, not the wider column edge. Not applied at >=760px, where the
     photo sits in its own column and the row spans the text column. */
  .profile__name-row { padding-right: calc((100% - 320px) / 2); }
}

/* ULTRA-NARROW: let the icon group take its own line.
   .profile__name-row is a single-line flex row (no flex-wrap is declared
   anywhere; nowrap is simply the initial value) whose BOTH children are
   flex-shrink:0 — .profile__name above, so "AYAGIRI TAKUYA" can never wrap
   at its single space, and .profile__quicklinks, so the tap boxes never
   shrink. With neither side able to give, the row's minimum content width
   is fixed at 145.66px of name plus 5 x 36px of icons = 325.66px, whatever
   the viewport. The row starts 24px in from the left (the section's own
   padding), so the icons run past the screen at 24 + 325.66 > viewport,
   i.e. at 349px and below; .section is overflow:hidden, so the result is
   the LINE mark being cut off rather than the page scrolling sideways —
   which is why the page never reported horizontal overflow. Measured at
   320px: 29.66px cut, LINE only 17.6% visible.
   349px is therefore the measured boundary, not a round number: 350px is
   the first width where the row's fixed 325.66px fits on screen, and
   every width from 350px up is left byte-for-byte as approved.
   Wrapping is all that is needed. Nothing is resized, reordered, hidden or
   scrolled: the icons keep their 36px tap boxes and their order, and the
   line simply folds under the name, which is the one adaptation the row's
   own structure allows. */
@media (max-width: 349px) {
  .profile__name-row { flex-wrap: wrap; }
}
/* Role tags (俳優 / ダンサー / ストリーマー / はちみつマイスター /
   随筆家) — the filled-pill treatment (honey-300 fill / wood-deep text
   / capsule radius) approved on Mobile in the VISUAL POLISH DELTA.
   RESPONSIVE PARITY (R1): this is now the one treatment at every
   viewport (Desktop's former text + hairline-underline variant is
   retired). ROLE BADGES EXPANSION: grew from 3 to 5 badges, same DOM/
   order/design at every viewport — content, order and visual treatment
   are never conditional on viewport; only how the one flex-wrap row
   folds is layout adaptation. Base values (font-size/gap/pill shape)
   are the former Mobile values verbatim, so <=759px is unchanged. */
.profile__genres {
  margin-top: 0.9rem;
  display: flex;
  flex-wrap: wrap;
  gap: 1.1rem;
}
@media (min-width: 760px) {
  /* Unconstrained, all 5 badges have room to sit on one row from
     ~1000px of column width up (measured) — .profile__genres itself
     never needed a width, so nothing forced that outcome; it just
     wasn't the intended reading. Capping the row's own width to fit
     exactly 3 badges (roles 1-3 sum to ~252.5px + 2 gaps at these
     fixed, non-fluid pill sizes) but not a 4th (needs 407px+) makes
     flex-wrap fold the same 5-item row into 3+2 on its own — no HTML
     line break, no JS, no reordering; only the wrap point moves if a
     future 6th badge is added. */
  .profile__genres { max-width: 300px; }
}
.profile__genre {
  font-family: var(--font-label);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  padding: 0.35em 0.9em;
  border-radius: 999px;
  background: var(--honey-300);
  color: var(--wood-deep);
}

/* The PROFILE lead copy: the one line the whole section exists to prove.
   Weight comes from the lead/main size contrast and surrounding
   whitespace, not from an oversized single block — HERO already owns
   "big," PROFILE is the quiet section right after it. */
.profile__statement {
  margin-top: 2.1rem;
  margin-bottom: 1.8rem;
}
.profile__statement-lead,
.profile__statement-main {
  display: block;
  font-family: var(--font-jp-display);
}
.profile__statement-lead {
  font-size: 1.06rem;
  font-weight: 500;
  color: #4e4030;
  letter-spacing: 0.015em;
}
.profile__statement-main {
  margin-top: 0.15em;
  font-size: clamp(1.32rem, 4.4vw, 1.9rem);
  font-weight: 700;
  line-height: 1.55;
  letter-spacing: 0.01em;
  color: var(--ink);
  white-space: nowrap;
}
@media (max-width: 640px) {
  /* Keep the punchline on its one intended line — at this width `vw`
     alone runs too small to clear clamp()'s floor, which forced an
     ugly mid-word wrap ("…掘りがちで / す。"). */
  .profile__statement-main { font-size: 1.32rem; }
}
@media (max-width: 360px) {
  .profile__statement-main { font-size: 1.18rem; }
}

.profile__text {
  margin-top: 1.4rem;
  color: var(--ink-dim);
}
.profile__text + .profile__text { margin-top: 1rem; }
.profile__text--closing {
  margin-top: 1.8rem;
  color: var(--ink);
}
.profile__text--nowrap-unit {
  white-space: nowrap;
}

/* PROFILE PHOTO PORTFOLIO (preview, TEMPORARY UNFREEZE) — a continuous
   editorial marquee between the intro copy and QUALIFICATIONS. See the
   HTML comment above the markup for the full loop/accessibility rationale;
   this block is the visual mechanics only.

   Confined to the same 920px column .profile__grid / .profile__qualifications
   already use (own max-width + overflow:hidden here, not a full-bleed
   breakout) so it can never be the thing that pushes the document's own
   scrollWidth past its clientWidth -- the strip clips its own oversized
   track locally, regardless of viewport. */
.profile-portfolio {
  max-width: 920px;
  margin: 3rem auto 0;
}
.profile-portfolio__strip {
  /* INTERACTION PARITY (preview): a real horizontal scroller rather than a
     clipped box, so the automatic movement (driven by writing scrollLeft in
     js/main.js) and the user's own drag act on the same property and cannot
     fight each other -- the same architecture the SNS strip already uses.
     touch-action stays at its default: the browser already pans this strip
     horizontally and scrolls the page vertically, and overriding it would
     only cost pinch-zoom. Visual result is unchanged: the scrollbar is
     hidden and nothing about the photos' size, gap or radius moves. */
  overflow-x: auto;
  overflow-y: hidden;
  /* html sets scroll-behavior:smooth; be explicit that per-frame scrollLeft
     writes here must land instantly, never animate. */
  scroll-behavior: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.profile-portfolio__strip::-webkit-scrollbar { height: 0; }
/* JS is present and driving the strip: drop the keyframes so the two
   mechanisms never both move the same track. Without JS the animation
   below still runs, so the photos are never left static-and-unscrollable. */
.profile-portfolio.is-manual .profile-portfolio__track { animation: none; }
.profile-portfolio__track {
  display: flex;
  align-items: center;
  width: max-content;
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 0;
  /* linear = constant speed, no ease-in/out -- a marquee should never
     visibly accelerate or decelerate. 60s is one full 10-photo sequence
     (README_ASSET_SPEC.md's target); translateX(-50%) is exactly half of
     THIS element's own width, and this track is deliberately twice the
     sequence's width (10 photos listed twice), so -50% always lands on
     precisely one sequence regardless of viewport -- no viewport-specific
     distance to keep in sync by hand. */
  animation: profilePortfolioMarquee 60s linear infinite;
}
@keyframes profilePortfolioMarquee {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
.profile-portfolio__item { flex: 0 0 auto; }
.profile-portfolio__img {
  display: block;
  height: 280px;
  width: auto;
  /* no object-fit: the point is each photo's own portrait/landscape/
     near-square shape, not a shared box every image gets cropped into */
  border-radius: 6px;
}
@media (min-width: 760px) {
  .profile-portfolio__img { height: 260px; }
}
@media (prefers-reduced-motion: reduce) {
  /* AUTO MOTION STOPS, but the 10 real photos stay fully reachable: the
     strip becomes a native horizontally-scrollable row (the same
     touch/trackpad scroll the browser gives any overflow:auto element,
     no JS) and the loop's own duplicate 10 -- meaningless without the
     animation that needed them -- are hidden rather than left as an
     inert, confusing second copy to scroll into. */
  .profile-portfolio__track { animation: none; }
  .profile-portfolio__strip { -webkit-overflow-scrolling: touch; }
  .profile-portfolio__track li[aria-hidden="true"] { display: none; }
}

/* QUALIFICATIONS — sits after .profile__text--closing (photo/name/role
   tags/statement/intro text all stay the lead; this is the last, quietest
   thing in PROFILE) and after .profile__grid as a sibling, not a grid
   child, so it never lands inside the two-column track. A thin honey-
   gold rule + small dot bullets + wood-deep text, reusing the same paper/
   editorial/honey/wood language as the rest of PROFILE — no card, no
   rounded box, no shadow, no table. RESPONSIVE PARITY (R1): formerly
   Mobile 390 only (display:none elsewhere) — the same 10 items are now
   shown at every viewport (content parity), full width under the grid
   (max-width 920px matches .profile__grid). Values below are the former
   Mobile values verbatim; the only >=760px adaptation is the two-column
   list at the end of this block. */
.profile__qualifications {
  display: block;
  max-width: 920px;
  /* POLISH 01: margin/padding widened further so the PROFILE body
     → QUALIFICATIONS handoff reads as a clear break, not a tight
     continuation. */
  /* HIERARCHY v2 (preview, TEMPORARY UNFREEZE): 4rem -> 3rem. Once the
     PHOTO PORTFOLIO landed between the intro copy and this block, the
     old 4rem read (Mobile especially) as a section break rather than the
     next beat of the same PROFILE story. 3rem is not a new number: it is
     .profile-portfolio's own margin-top, so the photo strip now carries
     equal air above and below instead of being pushed away from what
     follows it. padding-top stays 2.6rem -- only ONE lever moves, so the
     divider->heading breathing room below is untouched and the photo can
     never end up crowding the heading. */
  margin: 3rem auto 0;
  /* PATCH 001: divider -> title 2.6rem -> 2rem. Only the padding moves; the
     3rem margin above stays equal to the photo strip's own air. */
  padding-top: 2rem;
  /* POLISH DELTA 01 + POLISH 01: softened from the flat var(--honey-300)
     — same hue, progressively lower opacity, so entry/list rules read
     as paper/editorial rather than a table's ruled lines. */
  border-top: 1px solid rgba(244, 205, 124, 0.3);
}
/* HIERARCHY (preview, TEMPORARY UNFREEZE): raised from an eyebrow-scale
   label (0.76rem/0.68rem, barely bigger than .profile__qualifications-list
   itself at 0.85rem) to a real sub-heading -- still font-label/font-en-
   display and honey-text/ink-faint as before (no new colour, no new
   family), just bigger and heavier. Deliberately stays under
   .section__title's clamp(1.9rem,4.5vw,2.7rem) floor at every viewport
   (max 1.5rem here vs. its 1.9rem minimum) so PROFILE's own title is
   never outranked. letter-spacing is pulled in from the old label-sized
   values -- 0.12em/0.1em reads fine at 0.76rem but turns visibly gappy
   once the glyphs are this much bigger. */
/* HIERARCHY v2 (preview, TEMPORARY UNFREEZE): one further step up, since
   with the PHOTO PORTFOLIO now above it the v1 size was still not the
   first thing the eye lands on after the photos. Both parts scale by
   ~1.2x (JP 20/24px -> 24/28.8px, EN 13.6px -> 16px) -- the SAME factor
   on each, so "資格" primary / "QUALIFICATIONS" secondary keeps the exact
   ratio it already had (~0.57 desktop) rather than the English being
   quietly demoted. Family, colour and weight language are unchanged.
   Still bounded below .section__title at every viewport -- 28.8px max
   here against its 30.4px floor (320/390), 34.56px (768) and 43.2px
   (1024+) -- so PROFILE's own title stays the section's strongest voice. */
.profile__qualifications-title {
  font-family: var(--font-label);
  font-size: clamp(1.5rem, 4.2vw, 1.8rem);
  font-weight: 700;
  letter-spacing: 0.06em;
  /* D-2: --honey-text -> --honey-text-deep, the palette's own accessible variant
     of the same hue (:root §accessible text variants). The title is large text
     and passed either way; it moves so the pairing in §COLOR CONSISTENCY v3
     below still holds -- both halves of the heading stay on one token. */
  color: var(--honey-text-deep);
  margin-bottom: 2rem;
}
.profile__qualifications-en {
  margin-left: 0.7em;
  font-family: var(--font-en-display);
  font-size: 1rem;
  letter-spacing: 0.08em;
  /* COLOR CONSISTENCY v3 (preview, TEMPORARY UNFREEZE): was var(--ink-faint),
     an unrelated muted-brown token that made 資格/QUALIFICATIONS read as two
     differently-colored labels rather than one heading. Primary/secondary is
     already carried by font-size + font-weight + letter-spacing (§HIERARCHY
     above), so no color-based secondary treatment is needed -- reuse the
     JP title's own token so both halves read as one heading.
     D-2: that token is now --honey-text-deep. At 16px/700 this label is small
     text for AA, and --honey-text measured 3.91 on PROFILE's rendered ground;
     --honey-text-deep reaches 4.62 there and 4.89 at 1440, so the heading keeps
     its honey identity rather than dropping to ink. */
  color: var(--honey-text-deep);
}
.profile__qualifications-list {
  display: flex;
  flex-direction: column;
}
.profile__qualifications-list li {
  position: relative;
  padding: 0.6rem 0 0.6rem 1.1rem;
  /* POLISH DELTA 01 + POLISH 01: same softened rule as the entry border
     above — lighter than a full var(--honey-300) line so the list reads
     closer to paper/editorial spacing than a ruled table. */
  border-bottom: 1px solid rgba(244, 205, 124, 0.3);
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--wood-deep);
}
.profile__qualifications-list li:last-child { border-bottom: none; }
.profile__qualifications-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 1.05rem;
  width: 5px;
  height: 5px;
  border-radius: 999px;
  background: var(--honey-500);
}
@media (min-width: 760px) {
  /* Two columns once the block is wide enough (same 760px seam the
     PROFILE grid already uses — no new breakpoint): 10 short single-line
     items in one 920px-wide column read as a sparse table; two columns
     of five keep the same dot/hairline rhythm at a width that matches
     the grid above. column-gap = the grid's own 3.4rem column gap. The
     last ROW (two items) drops its hairline, mirroring :last-child in
     the single-column list. */
  .profile__qualifications-list {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 3.4rem;
  }
  .profile__qualifications-list li:nth-last-child(-n + 2) { border-bottom: none; }
}

/* =========================================================
   NEWS
   ========================================================= */
/* editorial list, not a card stack: a thin rule between rows carries
   the separation instead of border+shadow+radius on every item, so the
   section reads fast rather than "flashy" (WORKS owns that register) */
.news-list {
  display: flex;
  flex-direction: column;
  max-width: 780px;
  margin: 0 auto;
}
/* ROW LAYOUT. The same three-column row, applied to whichever element
   actually owns it: the <li> on an ordinary item, the <a> on a linked one.
   The link used to be display:contents so the <li> could keep owning the grid
   — visually correct, but an element with no box of its own is not reachable
   by Tab in Chromium (measured: tabindex="0" does not help either), so a NEWS
   item with a url could not be operated by keyboard at all. Giving the <a> the
   row instead makes it a normal focusable link AND makes the whole row its hit
   area; the <li> keeps the rule between rows and becomes a plain block. Every
   value below is the one the <li> already used, so the rendered row is
   unchanged. */
.news-item {
  padding: 0.85rem 0.2rem;
  border-bottom: 1px solid var(--border);
  display: grid;
  grid-template-columns: auto auto 1fr;
  align-items: baseline;
  gap: 1rem;
  transition: padding-left 0.2s ease, color 0.2s ease;
}
.news-item:first-child { border-top: 1px solid var(--border); }
/* The linked <li> is only a container now — the padding and the grid move to
   the <a> with it, so the row keeps exactly the same height and inset.
   These are separate rules rather than one selector list with .news-item, and
   that is deliberate: Chromium gives a selector list the specificity of its
   most specific member, so pairing the <li> and the <a> in one list made that
   list tie with .news-item--link's own padding:0 and win on order inside the
   media query below — measured, the mobile row kept the <li>'s padding and the
   link stopped covering it. */
.news-item--link { display: block; padding: 0; }
.news-item--link > .news-item__link {
  padding: 0.85rem 0.2rem;
  display: grid;
  grid-template-columns: auto auto 1fr;
  align-items: baseline;
  gap: 1rem;
  transition: padding-left 0.2s ease, color 0.2s ease;
}
/* OPTIONAL LINK SUPPORT: the existing subtle hover feedback (padding
   shift + title color change) still only fires for a NEWS item that
   actually has a url — an unlinked item stays plain, non-interactive.
   The shift now applies to the <a>, which is the element carrying the
   padding. */
.news-item--link:hover > .news-item__link,
.news-item--link:focus-within > .news-item__link {
  padding-left: 0.5rem;
}
.news-item--link:hover .news-item__title,
.news-item--link:focus-within .news-item__title {
  /* Was var(--honey-text). That gold measured 3.79 (390) / 3.82 (1440) against
     the ground NEWS actually paints -- --bg-soft under the Mobile honeycomb --
     so hovering or focusing a row made its title LESS readable than at rest
     (10.88), and this rule also drives the keyboard state via :focus-within.
     No honey token clears AA there: --honey-text-deep still measured 4.474 at
     390. --wood-deep is the warm token that passes with margin (7.41 / 7.47).
     The colour cue is quieter than the gold was; the interaction is still
     carried by the padding-left step above (3.2px -> 8px, which co-fires on
     every one of these states) and by the focus ring below. */
  color: var(--wood-deep);
}
/* KEYBOARD FOCUS. Now that the link has a box the ring goes straight on it —
   the previous :has() indirection existed only because display:contents left
   nothing to paint on. :focus-visible keeps it keyboard-only: a mouse click
   never leaves an outline. Same 2px honey ring the rest of the site uses
   (.tab-bar__btn, .career-archive__cat, .career-archive__close). */
.news-item__link:focus-visible {
  outline: 2px solid var(--honey-text-deep);
  outline-offset: 2px;
}
.news-item__link {
  color: inherit;
  text-decoration: none;
}
.news-item__date {
  font-family: "SF Mono", var(--font-label), monospace;
  font-size: 0.74rem;
  color: var(--ink-faint-text);
  font-variant-numeric: tabular-nums;
}
.news-item__tag {
  font-family: var(--font-label);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 0.25em 0.7em;
  border-radius: 999px;
  color: var(--wood-deep);
  background-color: var(--honey-300);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='12'%3E%3Cpolygon points='7,0 14,3 14,9 7,12 0,9 0,3' fill='none' stroke='%23000' stroke-opacity='0.1' stroke-width='0.6'/%3E%3C/svg%3E");
}
.news-item__tag[data-cat="EVENT"] { background: var(--honey-300); }
.news-item__tag[data-cat="STAGE"] { background: var(--accent-lavender); }
.news-item__tag[data-cat="MEDIA"] { background: var(--accent-sky); }
.news-item__tag[data-cat="STREAM"] { background: var(--accent-pink); }
.news-item__tag[data-cat="OTHER"] { background: var(--border-soft, #eee2c9); }
.news-item__title { font-size: 0.95rem; font-weight: 600; color: var(--ink); transition: color 0.2s ease; }
.news-item[hidden] { display: none; }
@media (max-width: 560px) {
  /* NEWS MOBILE FINAL POLISH 01 — DATE + CATEGORY share one row (a
     quiet metadata line), TITLE reads as its own headline line below,
     via grid-template-areas rather than restructuring the markup.
     .news-item__link staying display:contents (see above) means this
     works identically whether or not the row is wrapped in an <a>. */
  .news-item {
    grid-template-columns: auto 1fr;
    grid-template-areas: "date cat" "title title";
    align-items: center;
    column-gap: 1rem;
    row-gap: 0.4rem;
    padding: 0.7rem 0.2rem;
  }
  /* restated here, not inherited from the rule above: see the specificity note
     beside .news-item--link */
  .news-item--link { padding: 0; }
  .news-item--link > .news-item__link {
    grid-template-columns: auto 1fr;
    grid-template-areas: "date cat" "title title";
    align-items: center;
    column-gap: 1rem;
    row-gap: 0.4rem;
    padding: 0.7rem 0.2rem;
  }
  .news-item__date { grid-area: date; }
  .news-item__tag {
    /* was implicitly full-row-width once stacked to a single column;
       shrink back to its own content width instead of stretching. */
    grid-area: cat;
    justify-self: start;
  }
  .news-item__title { grid-area: title; }
}
/* NEWS READABILITY (VISUAL POLISH DELTA): reverted the previous
   text-shadow/halo approach — text itself is sharp again, un-styled.
   Instead the HONEYCOMB is quieted from the background side: a soft,
   blurred cream (var(--bg)) wash sits behind each row (per .news-item,
   so multi-line titles are covered naturally with no per-glyph
   trickery), fading out via blur before it reaches the row's own
   top/bottom gap — margins/section edges/below the list keep the full
   HONEYCOMB untouched. Not a card: no hard edge, no border, no shadow,
   same cream as the page itself. Mobile only, same breakpoint as the
   HONEYCOMB layer itself. */
@media (max-width: 759px) {
  .news-item { position: relative; }
  .news-item::before {
    content: "";
    position: absolute;
    inset: 2px 0;
    background: var(--bg);
    opacity: 0.6;
    filter: blur(9px);
    z-index: -1;
    pointer-events: none;
  }
}

.news-more {
  display: block;
  margin: 1.6rem auto 0;
  font-family: var(--font-label);
  font-weight: 700;
  font-size: 0.8rem;
  letter-spacing: 0.05em;
  padding: 0.65em 1.6em;
  border: 1.5px solid var(--wood);
  border-radius: 999px;
  color: var(--wood-deep);
  background: var(--surface);
  transition: background 0.2s ease, transform 0.2s ease;
}
.news-more:hover { background: var(--honey-300); transform: translateY(-2px); }
/* display:block above beats the UA sheet's [hidden] { display: none }, so the
   button JS hides after the last items are revealed stayed on screen, focusable
   and clickable while doing nothing. .news-item already carries the same guard
   below its own rules; this is the button's missing counterpart. */
.news-more[hidden] { display: none; }
.news-more:focus-visible {
  outline: 2px solid var(--honey-text-deep);
  outline-offset: 2px;
}

.news-list__loading {
  text-align: center;
  padding: 2rem 0;
  color: var(--ink-faint);
  font-family: var(--font-label);
  font-size: 0.8rem;
}
/* --ink-faint is 3.00:1 on --bg — below AA for text this size, and this class
   carries real user-facing copy ("現在お知らせはありません。" and the load-failure
   message), not just the loading placeholder. --ink-faint-text is the token the
   project already defines for exactly this (4.92:1 on --bg) and is what the NEWS
   date beside it uses. Scoped to .news-list on purpose: the SNS rail reuses this
   class for its own placeholder and error row, and SNS is FROZEN. */
.news-list .news-list__loading { color: var(--ink-faint-text); }

/* =========================================================
   WORKS
   ========================================================= */
/* v3.2: text + thin line + a small Honey Gold indicator — never a pill
   button. Tabs sit quietly under the section; the work itself stays the
   loudest thing on screen. */
/* CAREER ARCHIVE NAVIGATION: ALL | STAGE | EVENT | TV / FILM | CM |
   MEDIA. Six labels instead of four, so the row is explicitly nowrap —
   wrapping to a second line would turn a quiet index into a block of
   navigation, and the widest label ("TV / FILM") must not break at its
   slash either. The gap carries the whole width adjustment; the type
   size is untouched, since shrinking these labels to fit is what makes a
   nav read as a control panel. */
.tab-bar {
  display: flex;
  flex-wrap: nowrap;
  gap: 1.55rem;
  justify-content: center;
  margin-bottom: 3.2rem;
  border-bottom: 1px solid var(--border);
}
.tab-bar__btn {
  font-family: var(--font-label);
  font-size: 0.76rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  padding: 0 0 0.75em;
  color: var(--ink-faint-text);
  position: relative;
  transition: color 0.2s ease;
  flex: 0 0 auto;
  white-space: nowrap;
}
.tab-bar__btn::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--honey-500);
  transform: scaleX(0);
  transition: transform 0.2s ease;
}
.tab-bar__btn:hover { color: var(--honey-text); }
.tab-bar__btn.is-active { color: var(--wood-deep); }
.tab-bar__btn.is-active::after { transform: scaleX(1); }

/* Below the Desktop band the six labels can be wider than the screen (they
   are at 390 and narrower). The row slides sideways instead of wrapping or
   shrinking: the last label sits partly past the right edge, which is what
   tells the reader there is more of the index to reach. The bar is its own
   scroll container, so the PAGE never gains a horizontal scroll; the
   scrollbar is hidden because a visible one would redraw this quiet row as
   a widget (js/main.js revealWorksTab keeps the chosen tab in view).
   The row is centred by auto margins on the end items rather than by
   justify-content: with justify-content:center, content that overflows a
   scroll container spills past the START edge too and can never be
   scrolled back to. Auto margins collapse to 0 the moment the row is wider
   than the bar, so the same rule centres the six labels while they fit and
   leaves every one of them reachable once they do not — which is why this
   is scoped to the whole Mobile band and not to a measured pixel width:
   the real label widths depend on the webfont. */
@media (max-width: 759px) {
  .tab-bar {
    justify-content: flex-start;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    scroll-behavior: smooth;
    /* overflow-x on its own turns overflow-y into `auto` as well, which
       would clip the 1px of the active tab's 2px indicator that hangs
       below the content box — the marker would read a hairline thinner
       here than on Desktop. The padding takes that 1px inside the
       scrollport and the margin gives it straight back, so the indicator
       is identical at every width and nothing below the bar moves. */
    padding-bottom: 1px;
    margin-bottom: calc(3.2rem - 1px);
    /* A slightly tighter gap here only. Six labels then fit 390 outright,
       so nothing is cut at the most common phone width and the row does
       not have to be scrolled to read it. Narrower screens still scroll,
       and js/main.js revealWorksTab keeps what it leaves at the edges
       readable. Desktop spacing is untouched. */
    gap: 1.25rem;
  }
  .tab-bar::-webkit-scrollbar { width: 0; height: 0; }
  .tab-bar__btn:first-child { margin-inline-start: auto; }
  .tab-bar__btn:last-child { margin-inline-end: auto; }
}
@media (max-width: 759px) and (prefers-reduced-motion: reduce) {
  .tab-bar { scroll-behavior: auto; }
}

/* v3.1: ALL is SHOWCASE MODE (3 representative works, photo-forward
   editorial layout below) — STAGE/EVENT/TV・CM are ARCHIVE MODE (a
   compact, text-first, year-grouped list further down). Both render
   into the same #works-grid container, so a light cross-fade on that
   element covers switching between the two registers, not just
   between tabs within one register. */
#works-grid { transition: opacity 0.25s ease; }
#works-grid.is-fading { opacity: 0; }

/* PHASE W2 — RESPONSIVE PARITY: SHOWCASE is one horizontal rail at every
   viewport. The v3.3 Desktop 12-column grid (shape-keyed spans + a
   stepped nth-child offset for exactly 3 items) and the separate
   max-width:900px single-column collapse that sat between it and the
   Mobile rail were removed together: three layouts across two
   breakpoints were what produced the 900->901px cliff (section height
   4432px -> 1101px, photo 852x1195 -> 265x372) and what kept Desktop
   showing 3 of the 7 SHOWCASE works. The rail rule itself now lives
   further down this file, unscoped (search "WORKS FEATURED GALLERY"),
   so there is one layout and one dataset everywhere. The editorial
   stagger is deliberately NOT carried into the rail: in a scroller a
   vertical offset reads as misalignment, not composition, so all cards
   are treated equally and the differentiation comes from the posters
   themselves. Shape classes survive for the PHOTO aspect-ratios only
   (.work-item--portrait/square/landscape .work-item__photo below). */
.work-item {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

.work-item__photo {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--border);
  background: linear-gradient(155deg, var(--honey-300), var(--bg-soft));
  transition: opacity 0.2s ease;
  display: flex;
  align-items: center;
  justify-content: center;
  /* PHASE W3-B: on a two-sided card this box is the <button> that flips
     TITLE ⇄ CHARACTER (js/main.js buildWorkItemEl); on a one-photo card
     and on loop clones it stays a <div>. Every geometry rule keys off
     this class, so both render identically — the lines below only
     neutralise what a <button> would otherwise bring: UA padding,
     shrink-to-fit width, native appearance, and iOS' grey tap flash
     over the whole poster. Harmless on the <div>. */
  width: 100%;
  padding: 0;
  appearance: none;
  -webkit-appearance: none;
  -webkit-tap-highlight-color: transparent;
}
/* PHASE W3-A: hover only where hover exists. With the side button now
   inside the card, a tap on a touch screen would otherwise leave this
   dim stuck on the photo until the next tap somewhere else. Pointer
   devices see exactly the same 0.9 as before. */
@media (hover: hover) {
  .work-item:hover .work-item__photo { opacity: 0.9; }
}
/* contain, not cover: the real posters carry title logos and credit text
   at their own edges (see the brief) that a crop would clip — every
   SHOWCASE photo box below is sized close to its source image's own
   aspect ratio specifically so this shows the full frame with minimal
   letterboxing, never cropping away people/logos/credits. */
.work-item__photo img { width: 100%; height: 100%; object-fit: contain; object-position: center; display: block; }
.work-item--portrait .work-item__photo { aspect-ratio: 3 / 4; }
.work-item--landscape .work-item__photo { aspect-ratio: 16 / 9; }
.work-item--square .work-item__photo { aspect-ratio: 1 / 1; }

/* per-item aspect-ratio matched to the real source images so contain
   never has to letterbox more than a sliver:
   01 アイ★チュウ — key visual 960x1347 / role visual 1463x2048, both ~0.713
   02 カイジ — key visual 1240x1740 (~0.713) crossfading with a 240x360
      (~0.667) role photo; 0.70 splits the difference so neither frame
      loses much to letterboxing
   03 ヨルハ — key visual 613x860 (~0.713) crossfading with a 363x512
      (~0.709) role photo (ADD YORHA ROLE VISUAL delta), close enough
      that 0.711 splits the difference
   04 VAZZROCK STAGE (Mobile SHOWCASE only, ADD 2 WORKS DELTA 01) — KV
      718x1000 (~0.718) crossfading with a 572x800 (~0.715) role photo,
      close enough that either value works; 0.716 splits the difference
   06 アルカナ・ファミリア (Mobile SHOWCASE only, ADD ARCANA FAMIGLIA
      delta) — KV 612x859 (~0.7125) crossfading with a 600x900 (~0.667)
      role photo; 0.69 splits the difference, same shape of compromise
      as 02 カイジ above.
   05 僕ら的には理想の落語 — ASSET REPLACEMENT delta: back to a normal
      portrait ROLE VISUAL (was briefly a landscape photo, handled via
      .work-item--landscape-role — see that block below, still intact
      and reusable for a future work, just no longer applied to this
      one via item.landscapeRole in data/works.json). KV 453x640
      (~0.708) crossfading with the new 600x800 (~0.75) role photo;
      0.73 splits the difference.
   LOOP GALLERY DELTA: switched from :nth-child to [data-work-id] —
   Mobile's loop gallery prepends/appends clone cards (see js/main.js
   setupWorksLoopGallery), which shifts every real card's nth-child
   position, but never changes each card's own data-work-id (set by
   buildWorkItemEl/buildCloneEl from item.id in data/works.json) — a
   clone shares its original's id, so it automatically gets the same
   aspect-ratio with zero extra rules. Desktop's separate nth-child
   grid-column rules above are untouched (Desktop's grid never gets
   clones). */
.work-item[data-work-id="ichu"] .work-item__photo { aspect-ratio: 0.713; }
.work-item[data-work-id="kaiji"] .work-item__photo { aspect-ratio: 0.70; }
.work-item[data-work-id="yoruha"] .work-item__photo { aspect-ratio: 0.711; }
.work-item[data-work-id="vazzrock"] .work-item__photo { aspect-ratio: 0.716; }
.work-item[data-work-id="arcana"] .work-item__photo { aspect-ratio: 0.69; }
.work-item[data-work-id="bokuraku"] .work-item__photo { aspect-ratio: 0.73; }
.work-item[data-work-id="starrysky"] .work-item__photo { aspect-ratio: 0.71; }

/* LANDSCAPE ROLE — reusable modifier (POLISH DELTA 01) for any SHOWCASE
   item whose role visual is landscape while its key visual is portrait
   (currently only 僕ら的には理想の落語; item.landscapeRole in
   data/works.json opts an item in, js/main.js adds this class). The
   card's own outer shape goes back to the gallery's normal portrait
   rhythm — the previous ~1.03 near-square box made this one card read
   as a different height from its neighbors. Inside that taller box,
   only the 2nd frame (the landscape role visual — the 1st/KV frame is
   already portrait and needs no special treatment) is inset into a
   smaller "mat" area sized close to the photo's own ~1.5 aspect ratio,
   with a thin honey-gold line around it — a quiet framed-print
   treatment using the same paper/wood/honey gradient the photo box
   already had as its background, not a new asset or a loud frame. */
.work-item--landscape-role .work-item__photo { aspect-ratio: 0.85; }
.work-item--landscape-role .work-item__frame:nth-of-type(2) {
  /* explicit top/left/width/height rather than inset+auto — a replaced
     element (img) with width/height both auto and an intrinsic ratio
     follows a different (contain-like, top/left-anchored) sizing
     algorithm than a normal box, which fought with object-fit here and
     left the mat far narrower than intended; explicit percentages size
     the box itself deterministically, then object-fit:contain (inherited)
     fits the image inside it normally. */
  top: 25%;
  left: 6%;
  width: 88%;
  height: 50%;
  border: 1px solid var(--honey-500);
  transform: scale(0.98);
  /* PHASE W3-A: same 0.45s as the .work-item--crossfade frames below —
     restated here rather than inherited since this selector is more
     specific and would otherwise win with a different value */
  transition: opacity 0.45s ease, transform 0.45s ease;
}
.work-item--landscape-role .work-item__frame:nth-of-type(2).is-active { transform: scale(1); }
@media (prefers-reduced-motion: reduce) {
  /* matches the existing global rule for every other crossfade frame:
     instant swap, no transition, no scale motion at all */
  .work-item--landscape-role .work-item__frame:nth-of-type(2) { transition: none; transform: none; }
}

/* Two-sided card frames (TITLE visual / CHARACTER visual): stacked
   exactly on top of one another; js/main.js setWorkItemSide() moves
   .is-active, and since both the outgoing and incoming frame transition
   at once, this reads as a single quiet cross-fade rather than a
   slide/carousel. PHASE W3-A: the fade is user-initiated now (the
   automatic 1.5s Desktop / 1s Mobile cycle is retired), so ONE duration
   at every viewport, 0.45s — quick enough not to feel like waiting on a
   button, slow enough that the other side is seen to arrive rather than
   to snap in. Under prefers-reduced-motion the swap is instant (below);
   the interaction itself is never disabled. */
.work-item--crossfade .work-item__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity 0.45s ease;
}
.work-item--crossfade .work-item__frame.is-active { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .work-item--crossfade .work-item__frame { transition: none; }
}

/* D-5A: .work-item__num — the sequential number that sat in the poster's
   top-left corner — is removed, markup and all (js/main.js no longer emits
   it). It was 9.92px of rgba(255,253,248,0.9) painted straight onto whatever
   each poster had in that corner, so it was legible on a dark key visual and
   invisible on a pale one: measured per glyph pixel, 72-99% of it fell below
   AA on the pastel posters while a dark one passed at 8.4. Its text-shadow
   could not fix that -- a dark shadow sharpens the edge of light text but
   leaves the glyph body's contrast untouched. The number was decorative
   reading order, carried no information the card doesn't already give, and
   was absolutely positioned, so nothing about the card's geometry depended
   on it. */

.work-item__meta { display: flex; flex-direction: column; gap: 0.3rem; }
.work-item__title {
  font-family: var(--font-en-display);
  font-weight: 600;
  font-size: clamp(1.02rem, 1.4vw, 1.3rem);
  line-height: 1.32;
  color: var(--ink);
}
/* PHASE W3-B: the category / year line (.work-item__sub / __cat / __sep /
   __year, "STAGE / 2017–2026") is no longer rendered in SHOWCASE at any
   viewport — a product decision, not a Mobile/Desktop split: SHOWCASE is
   title + role under an interactive poster, ARCHIVE (.works-archive__*
   below) is where year and category live. Data untouched. */
.work-item__role { font-size: 0.84rem; color: var(--ink-dim); }

.work-item__visual-toggle:focus-visible {
  /* the whole visual frame is the focus target; inset like the site's
     other in-rail focusables (.profile__photo-track, .social-rail__link)
     because the rail clips anything drawn outside its box */
  outline: 2px solid var(--honey-text-deep);
  outline-offset: -3px;
}
/* ...but on this control that outline is drawn and then covered. The two
   poster visuals inside the button are .work-item__frame, which is
   position:absolute; inset:0 — positioned descendants paint in step 8 of
   the CSS 2.1 Appendix E order, above the element's own outline in step 7
   — so the inset ring ends up underneath the photograph and measures 0%
   of its own colour on screen. The outline above is kept (it is what
   forced-colors mode re-colours, and it is the ring wherever nothing
   covers it); this pseudo-element repaints the SAME ring after the
   frames, as the button's last child in tree order, so it lands above
   them without giving any descendant a z-index.
   .work-item__photo is position:relative with a 1px border and no padding,
   so the overlay's inset is measured from the padding box — 1px inside the
   border box. Nothing moves, and nothing is drawn unless :focus-visible
   matches, so the normal state is untouched.

   BATCH 2D — one colour cannot carry it. Once the ring was actually
   painted it was measurable, and a single --honey-text-deep band clears 3:1
   on only 4 of 7 synthetic grounds (worst 1.37 at #808080): mid-tones defeat
   deep amber exactly as they defeat any single hue. So the overlay becomes a
   light+dark pair, --surface over --ink, the palette's existing extremes. No
   new token, and the honey outline above is deliberately left in place as the
   site's focus identity and as the band forced-colors mode re-colours.
   WHY inset:2px AND NOT inset:0. The pair only helps if BOTH bands have
   artwork to contrast against. Flush against the padding box the outer band
   abuts the card's own 1px --border instead of the poster, which leaves the
   inner band carrying every ground alone -- measured 5 of 7, still failing
   #aaaaaa and #dddddd. Holding the overlay 2px in leaves a 2px margin of
   poster outside it, so the outer band faces artwork and the inner band faces
   artwork: 7 of 7 grounds, lowest 3.89, every ground carried by a band
   directly adjacent to it rather than by the two bands contrasting with each
   other.
   WHY LIGHT OUTSIDE. Both orders measure identically against flat grey, so
   the real posters decided it: --surface outside scores 97% of perimeter
   samples at 3:1 across all seven toggles against 88% for the reverse, with
   the worst sample 1.64 against 1.15 and far less spread per card.
   4px of ink in total, held inside the poster frame; no radius, no geometry
   and no layout can change, since the element is absolutely positioned and
   only exists while :focus-visible matches. */
.work-item__visual-toggle:focus-visible::after {
  content: "";
  position: absolute;
  inset: 2px;
  border: 2px solid var(--surface);
  box-shadow: inset 0 0 0 2px var(--ink);
  pointer-events: none;
}

/* PHASE W3-B — MASCOT TAP TUTORIAL (js/main.js setupWorksTutorial). The
   owner's "mascot + down-right arrow + TAP" PNG, shown once. MOTION
   POLISH: it lives in the #works section, not in the card — the rail is
   a scroll box and clips anything above or left of its edge — and the
   JS places it from the first poster's live rectangle so the mascot
   body peeks in from OUTSIDE the poster's top-left corner, over the
   cream ground, with only the arrow's tip inside the picture (width,
   left and top are set inline by positionWorksTutorial; the PNG's own
   aspect ratio is untouched, height:auto). Not a control:
   pointer-events:none lets the tap it invites land on the poster button
   beneath and keeps a swipe that starts on it the rail's swipe. Base
   state is invisible — the JS adds .is-active only once the card is
   actually on screen. */
.work-item__tutorial {
  position: absolute;
  height: auto;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
}
/* the guide is a direct child of #works now, and the Mobile section-world
   rule "#works > * { position: relative; z-index: 1 }" (above) would
   otherwise win on specificity and drop it into the flow at the bottom
   of the section — restate the two properties it fights under an id
   selector, at every viewport. */
#works > .work-item__tutorial {
  position: absolute;
  z-index: 3;
}
/* fade in, three "トン" nudges toward where the arrow points (9px
   down-right, ~13px on the diagonal), a short rest, fade out. 4.2s
   total: 0.4s in, 3 × (0.5s out + 0.5s back), 0.3s rest with the last
   "トン" fully returned, 0.5s out. Translation only — no bounce,
   spring, overshoot, rotation or scale: a pointer, not a bounce. */
.work-item__tutorial.is-active {
  animation: works-tutorial-tap 4.2s ease-in-out both;
}
@keyframes works-tutorial-tap {
  0%    { opacity: 0; transform: translate(0, 0); }
  9.5%  { opacity: 1; transform: translate(0, 0); }
  21.4% { transform: translate(9px, 9px); }
  33.3% { transform: translate(0, 0); }
  45.2% { transform: translate(9px, 9px); }
  57.1% { transform: translate(0, 0); }
  69%   { transform: translate(9px, 9px); }
  81%   { transform: translate(0, 0); }
  88.1% { opacity: 1; transform: translate(0, 0); }
  100%  { opacity: 0; transform: translate(0, 0); }
}
/* reduced-motion path (chosen by the JS, not by a media query alone, so
   the exit is timer-driven and can never wait on an animationend that
   will not come): still guide, then a plain fade */
.work-item__tutorial.is-static {
  opacity: 1;
  transition: opacity 0.5s ease;
}
.work-item__tutorial.is-static.is-leaving { opacity: 0; }
/* early dismiss: the user pressed a poster — a short fade, no ceremony */
.work-item__tutorial.is-dismissed {
  animation: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
@media (prefers-reduced-motion: reduce) {
  /* belt and braces: even if .is-active were ever applied here, no
     position movement — the element stays at its invisible base and the
     JS safety timer ends the guide. */
  .work-item__tutorial.is-active { animation: none; }
}

@media (max-width: 640px) {
  .works-grid { gap: 2.4rem; }
  /* the featured work stays visibly the biggest even in a single
     column — everything else keeps its own genre ratio, so portrait /
     landscape / square becomes the scroll rhythm itself */
  .work-item--feature .work-item__photo,
  .work-item--feature.work-item--portrait .work-item__photo { aspect-ratio: 4 / 5; }
}

/* WORKS FEATURED GALLERY — HORIZONTAL PORTFOLIO RAIL. SHOWCASE mode only
   (the #works-grid element carries the class "works-grid" in that mode,
   "works-archive" in ARCHIVE mode — this block targets the former only,
   so STAGE/EVENT/TV・CM stay the existing vertical year-grouped list,
   untouched). Native overflow-x + scroll-snap, no carousel library.
   Card width/align is shared with SNS's own SELECT DIAL gallery (70%
   width + center snap) so both horizontal galleries have one
   operational grammar — the active card centered with a peek of the
   previous AND next card on either side.
   PHASE W2: formerly scoped to max-width:759px with Desktop keeping a
   12-column grid. Now unscoped — the same rail runs everywhere, and
   only the card WIDTH changes above 760px (fixed 340px, see the media
   query at the end of this block) because a viewport-percentage card
   keeps growing into a poster-sized image on a wide screen. Placed
   after the max-width:640px .works-grid { gap: 2.4rem } rule on
   purpose: equal specificity, so document order keeps gap:1rem winning
   at every width exactly as it did before this block was unscoped. */
.works-grid {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  /* SCROLL INDICATOR UNIFICATION: the browser's own horizontal scrollbar
     (a classic bar on desktop, iOS Safari's grey overlay bar right under
     the posters) showed the same position the LEVER below shows, so the
     rail read as two indicators. Hidden here with the treatment the
     PROFILE / SNS marquees already use; the rail stays overflow-x:auto and
     focusable, so touch, wheel, trackpad and arrow-key scrolling are all
     untouched -- only the redundant drawing goes. */
  scrollbar-width: none;
  padding-bottom: 0.2rem;
  /* carried over from the deleted v3.3 grid rule, which declared it for
     every viewport — the Mobile rail inherited it and never reset it, so
     cards kept their own natural height instead of flex's default
     stretch. Restated here so unscoping the rail changes nothing at
     Mobile beyond the year/category line this round deliberately adds. */
  align-items: start;
}
.works-grid::-webkit-scrollbar { height: 0; }
.works-grid > .work-item {
  flex: 0 0 70%;
  scroll-snap-align: center;
  /* OVER-SCROLL FIX: `mandatory` only guarantees the scroll LANDS on a snap
     point, never that it lands on the NEAREST one, so a fast flick used to
     coast straight past several cards. Measured at 390: a strong flick
     carried scrollLeft 459 -> 1941 (+1482px = 5.8 snap intervals), which
     reached the trailing boundary clones and left handleWorksLoopSettle to
     correct 1941 -> 203.5 a moment later — read by the visitor as the
     carousel running away and then jumping backwards. With `always` the
     same flick travelled 511px (two cards), never entered the clone region
     and fired no correction, while light and normal swipes stayed on
     exactly one card as before. It CONTAINS the runaway rather than
     capping every gesture at one card: at 320/390 a hard flick still
     crosses two, and four consecutive hard flicks still reached a clone
     once. Ignored by browsers without support (< Safari 15), where
     behaviour is simply what it is today. */
  scroll-snap-stop: always;
}
/* LOOP GALLERY DELTA: boundary clones (js/main.js setupWorksLoopGallery)
   are a purely transient visual — the user glimpses one for a moment
   before the scroll position is silently corrected onto the identical
   real card at the other end of the strip — so they carry no
   interaction of their own. */
.work-item--clone { pointer-events: none; }

/* LEVER — a small honey-gold handle sliding along a wood-toned rail,
   position synced to #works-grid's real scrollLeft (js/main.js
   updateWorksGalleryLever). It is the rail's ONE scroll indicator: the
   browser scrollbar is hidden on .works-grid above, and the handle now
   reads as a plain position bar (left end = first work, right end =
   last). Only shown in SHOWCASE mode (sibling selector below);
   ARCHIVE mode's vertical list has no horizontal scroll to indicate. */
.works-gallery__lever {
  display: none;
  position: relative;
  max-width: 320px;
  margin: 1.5rem auto 0;
}
#works-grid.works-grid ~ .works-gallery__lever { display: block; }
.works-gallery__lever-track {
  display: block;
  height: 3px;
  border-radius: 999px;
  background: var(--wood);
  opacity: 0.3;
}
.works-gallery__lever-handle {
  position: absolute;
  top: 50%;
  left: 0;
  width: 30%;
  height: 7px;
  border-radius: 999px;
  background: var(--honey-500);
  border: 1px solid var(--honey-600);
  transform: translateY(-50%);
}
@media (min-width: 760px) {
  /* PHASE W2 — card width above the Mobile band. 70% of the viewport is
     the right card size while the viewport IS roughly one card wide; on
     a desktop it would render a 1000px poster. 340px keeps the photo at
     340x477 at every width from 760 up, which is close to the printed
     area the old 12-column grid gave each card, and lets ~2 cards show
     at 760, ~2.4 at 900 and ~3.2 from 1280 up. Width only — gap, snap,
     aspect-ratio, object-fit and the crossfade are shared with Mobile. */
  .works-grid > .work-item { flex-basis: 340px; }
}

/* ARCHIVE MODE (STAGE / EVENT / TV・CM): "look something up," not "look
   at the work" — text-first, grouped by year, no photo grid. Distinct
   from NEWS's flat date-sorted rows: this groups under a year heading,
   and pairs title with role via a leader line rather than a category
   tag, since the tab itself already states the category. */
/* CAREER ARCHIVE ROWS — the year-grouped list the fullscreen ledger
   (.career-archive, below) is built from; js/main.js buildArchiveGroups
   produces exactly this markup. Since FULLSCREEN v1.1 nothing else
   renders it: the in-page CAREER WINDOW (a scroll box on #works-grid
   with a masked lower edge and a scripted year handoff) is retired, so
   these rules describe the rows on a full page and nothing more. */
/* The run-out between years sits on the list INSIDE the year block, not
   as the year block's own padding: a sticky child is held inside its
   containing block's CONTENT box, so padding on the year block would
   release the heading 2.4rem early. */
.works-archive__year:not(:last-child) .works-archive__list { padding-bottom: 2.4rem; }
.works-archive__year:first-child { padding-top: 0.6rem; }

/* YEAR AS THE TIME HEADING. The year holds the top of the ledger while
   its own works pass beneath it and lets go the moment the next year
   arrives. It sticks directly under the ledger's category row (top =
   the row's measured height, --career-bar-h, set by js/main.js
   syncCareerBar) on an opaque cream box with its own head-room, so a
   row leaving upward passes under the year, then under the category
   row, and is never seen cut in half — no fade, no script.
   Two layout facts keep that true:
   1. The heading carries NO bottom margin. Sticky releases a heading when
      the bottom of its year block reaches the heading's MARGIN box, so
      the space between rule and first row lives on the list as
      padding-top, not here.
   2. The list's padding-bottom (2.4rem = 38.4px) is the run-out the last
      row needs to be fully under the heading before the heading is
      released: release happens when the block's bottom meets the
      heading's bottom, and the heading (0.55rem head-room + 0.84rem/1.4
      year + 0.3rem + 1px rule ≈ 33px) is shorter than the run-out. */
.works-archive__year-label {
  position: sticky;
  top: var(--career-bar-h, 0px);
  z-index: 2;
  margin-bottom: 0;
  padding-top: 0.55rem;
  background: var(--bg);
}
.works-archive__year-num {
  display: block;
  font-family: var(--font-mono);
  /* the year keeps its rank through what it is — mono, tracked,
     honey-coloured, ruled — not through its size */
  font-size: 0.84rem;
  line-height: 1.4;
  letter-spacing: 0.14em;
  color: var(--honey-text-deep);
  padding-bottom: 0.3rem;
  border-bottom: 1px solid var(--border);
}
.works-archive__list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding-top: 0.55rem; /* rule → first row; on the list, not the heading — see the lane note */
}
/* HIGH DENSITY — the row is a line of a career list, not a card. The
   body's 1.9 leading is set for reading paragraphs; a list of titles is
   scanned, so the row carries its own leading (1.45) at a reading size
   (title 0.95rem, role 0.76rem): a one-line row is ~29px of pitch, so a
   phone shows 8–12 works at a time and a year with six entries reads as
   a short block, not a wall.
   Wraps only when it has to (a long title on a narrow screen) rather
   than always stacking to two lines. Where the role does wrap (Mobile)
   it sits flush-left directly under the title — space-between puts a
   lone item at the start — so the credit is read as part of its title,
   not as a second right-aligned column drifting away from it; on the same
   line it keeps the right edge, as before. */
.works-archive__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  column-gap: 0.75rem;
  row-gap: 0;
}
.works-archive__title {
  font-family: var(--font-en-display);
  font-weight: 600;
  font-size: 0.95rem;
  line-height: 1.45;
  color: var(--ink);
}
.works-archive__role {
  font-family: var(--font-label);
  font-size: 0.76rem;
  line-height: 1.45;
  color: var(--ink-faint-text);
}
/* A row with no credit is a title and nothing else: the empty role box
   still had a full line of height, and on Mobile — where a wrapped title
   pushed it onto a line of its own — that was a blank line under a third
   of the entries. */
.works-archive__role:empty { display: none; }
/* Desktop / Tablet: TITLE ———— ROLE on one line, always. The width is
   there, so the row never breaks: a long title wraps inside its own box
   and the role stays on the right, on the title's first baseline. The
   role cannot be squeezed (flex-shrink: 0) so the title is what absorbs
   the width; max-width is only the safety net for a credit far longer
   than any on file. */
@media (min-width: 760px) {
  .works-archive__row { flex-wrap: nowrap; }
  .works-archive__role {
    flex-shrink: 0;
    max-width: 40%;
    text-align: right;
  }
}

/* =========================================================
   WORKS — CAREER ARCHIVE FULLSCREEN (PROTOTYPE v1)
   The career as a whole page rather than a window onto one: pressing a
   category tab opens the full list over the site, the way a ledger is
   opened, and × closes it onto exactly the WORKS the reader left. It
   keeps the section's own language — the same eyebrow, title and
   category row, and the CAREER ARCHIVE ROWS above (.works-archive__*),
   with the whole screen to lay them on, so the rows sit at a reading
   size and still show 8–16 works at a time. Plain cream, the paper texture at the site's usual 3%, no panel,
   no card, no frame: the information is the surface.
   Layout: the dialog is fixed and never scrolls itself; the __scroll
   child is the scroll region (its own overscroll, so a fling at the end
   of the list never moves the page behind), and the close button is
   placed against the dialog box, so it stays put at the top-right at
   every scroll position. The category row sticks to the top of the
   scroll region; the year headings stick directly beneath it (their
   `top` is the row's measured height, set by js/main.js) with an opaque
   cream box, so a row leaving upward passes under the year, then under
   the category row, and is never seen cut in half — the handoff needs
   no fade here. z-index 300 sits above HONEY (45), the TOP buttons
   (120) and the MENU trigger (210): while the ledger is open, nothing
   of the page shows through. */
.career-archive {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: var(--bg);
  --career-bar-h: 52px; /* measured at open/resize by js/main.js */
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.22s ease, transform 0.22s ease;
}
.career-archive[hidden] { display: none; }
.career-archive.is-open { opacity: 1; transform: none; }
.career-archive::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("../assets/graphics/paper-texture-f.webp");
  background-size: cover;
  background-position: center;
  opacity: 0.03;
  pointer-events: none;
}
/* the page behind: frozen in place, not pushed — scrollY is kept by the
   browser under overflow:hidden and put back on close by js/main.js */
html.is-career-open,
html.is-career-open body { overflow: hidden; }
.career-archive__scroll {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.career-archive__inner {
  max-width: 800px;
  margin: 0 auto;
  padding: 0 1.5rem calc(3.5rem + env(safe-area-inset-bottom, 0px));
}
/* header: the section's own head, left-aligned, kept clear of the × */
.career-archive__head {
  padding-top: calc(1.4rem + env(safe-area-inset-top, 0px));
  padding-right: 60px;
  margin-bottom: 1.2rem;
}
.career-archive__head .section__title { font-size: clamp(1.45rem, 4vw, 2.2rem); line-height: 1.15; }
/* category row — sticky, opaque, and clear of the × on its right */
.career-archive__bar {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--bg);
  /* 44px keeps the row clear of the 48px × (right:10px) on Mobile while
     leaving the five labels room to fit 390 without a cut letter */
  padding: 0.7rem 44px 0 0;
}
.career-archive__nav {
  display: flex;
  flex-wrap: nowrap;
  gap: 1.1rem; /* five labels fit 390; narrower screens slide the row */
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 1px; /* keeps the 2px indicator inside the scrollport */
}
.career-archive__nav::-webkit-scrollbar { width: 0; height: 0; }
/* the same quiet index as the WORKS tabs: text, thin line, honey mark */
.career-archive__cat {
  font-family: var(--font-label);
  font-size: 0.76rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  padding: 0.35em 0 0.75em;
  color: var(--ink-faint-text);
  position: relative;
  flex: 0 0 auto;
  white-space: nowrap;
  transition: color 0.2s ease;
}
.career-archive__cat::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--honey-500);
  transform: scaleX(0);
  transition: transform 0.2s ease;
}
.career-archive__cat:hover { color: var(--honey-text); }
.career-archive__cat[aria-pressed="true"] { color: var(--wood-deep); }
.career-archive__cat[aria-pressed="true"]::after { transform: scaleX(1); }
.career-archive__cat:focus-visible {
  outline: 2px solid var(--honey-text-deep);
  outline-offset: 2px;
}
/* the ledger itself: the CAREER ARCHIVE ROWS above, as they are */
.career-archive__list { padding-top: 0.3rem; }
.career-archive .works-list__loading { padding: 3rem 0; }
/* HONEY is under the ledger anyway (z 45 < 300); not painting it while
   the ledger is open saves its drop-shadow layer for nothing lost */
html.is-career-open .honey-drip,
html.is-career-open .honey-v2-drop-layer { visibility: hidden; }
/* × — always visible, always in the same place; a solid honey-orange disc so
   the CLOSE affordance reads at a glance over the rows that scroll beneath it.
   Part of the shared GLOBAL CLOSE UI language with
   .menu-trigger[aria-expanded="true"] (the single MENU/CLOSE control,
   further up in this file, next to the shared @keyframes closeUIRipple used
   here) -- same colour family, same
   glow/ripple mechanics -- kept as its own selector rather than combined
   with it (CLAUDE.md history: a combined selector list for two different
   structural contexts is exactly what broke NEWS's row padding once; two
   contexts this different, WORKS/MENU stay split). --honey-text-deep is
   reused as-is (already the palette's accessible-tuned deep amber, 4.92:1 on
   --bg); the hover tone and the glow's rgba form are new only because
   nothing else in the sheet needed them. */
.career-archive__close {
  position: absolute;
  top: calc(10px + env(safe-area-inset-top, 0px));
  right: calc(10px + env(safe-area-inset-right, 0px));
  z-index: 5;
  width: 48px;
  height: 48px;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--surface);
  background: var(--honey-text-deep);
  border: 1px solid var(--honey-text-deep);
  box-shadow: 0 0 16px 2px rgba(232, 169, 59, 0.45);
  transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.career-archive__close svg { width: 30px; height: 30px; display: block; position: relative; z-index: 1; }
.career-archive__close:hover {
  background: #7c4a08;
  border-color: #7c4a08;
  box-shadow: 0 0 20px 3px rgba(232, 169, 59, 0.6);
}
.career-archive__close:focus-visible {
  outline: 2px solid var(--honey-text-deep);
  outline-offset: 3px;
}
/* outer ripple -- two rings on the same keyframe, staggered by half the cycle
   so one is always mid-expansion (a "sonar" rather than a single flash).
   inset:0 + border-radius:inherit means each ring starts exactly the button's
   own size (invisible, hidden under the solid fill) and only becomes visible
   once it scales past the button's own edge -- z-index:-1 keeps the × always
   on top. No new DOM, no JS: content-less pseudo-elements only. */
.career-archive__close::before,
.career-archive__close::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  border: 2px solid var(--honey-500);
  opacity: 0;
  pointer-events: none;
  animation: closeUIRipple 3s ease-out infinite;
}
.career-archive__close::after { animation-delay: 1.5s; }
@media (min-width: 760px) {
  .career-archive__close {
    top: calc(18px + env(safe-area-inset-top, 0px));
    right: calc(18px + env(safe-area-inset-right, 0px));
    width: 56px;
    height: 56px;
  }
  .career-archive__close svg { width: 32px; height: 32px; }
  .career-archive__head { padding-top: 2.2rem; padding-right: 80px; }
  .career-archive__bar { padding-right: 80px; }
  .career-archive__nav { gap: 1.25rem; }
  .career-archive__inner { padding-left: 2rem; padding-right: 2rem; }
}
/* v1.1 — Mobile (and any short, landscape-like viewport): the head is a
   label, not a section opener. The eyebrow, title and 出演歴 stay, one
   notch smaller and closer, so the first rows start ~140px from the top
   instead of ~215px and the list is what the screen is for. */
@media (max-width: 759px), (max-height: 500px) {
  .career-archive__head {
    padding-top: calc(0.9rem + env(safe-area-inset-top, 0px));
    margin-bottom: 0.55rem;
  }
  .career-archive__head .section__eyebrow { font-size: 0.62rem; }
  .career-archive__head .section__eyebrow::after { display: none; }
  .career-archive__head .section__title { font-size: 1.35rem; margin-top: 0.3rem; }
  .career-archive__head .section__title-jp { font-size: 0.72rem; letter-spacing: 0.22em; margin-top: 0.3rem; }
  .career-archive__bar { padding-top: 0.45rem; }
}
/* v1.1 — below 360px the five labels cannot share one line beside the ×;
   rather than a sliding row that shows a cut letter, they take two lines
   (STAGE EVENT TV / FILM · CM MEDIA), every label whole and tappable. */
@media (max-width: 359px) {
  .career-archive__nav {
    flex-wrap: wrap;
    overflow: visible;
    column-gap: 1rem;
    row-gap: 0.1rem;
  }
}
@media (prefers-reduced-motion: reduce) {
  .career-archive { transition: none; transform: none; }
  /* ripple stopped; the solid orange fill + static box-shadow glow above
     stay, so CLOSE is still immediately findable without any animation */
  .career-archive__close::before,
  .career-archive__close::after { animation: none; opacity: 0; }
}

.works-list__loading {
  grid-column: 1 / -1;
  text-align: center;
  padding: 2rem 0;
  color: var(--ink-faint);
  font-family: var(--font-label);
  font-size: 0.8rem;
}

/* =========================================================
   SNS / SOCIAL MEDIA — "SIGNAL RAIL" (LOGO ONLY) — FROZEN
   Approved over the earlier "SNS WORLD" honey-jar concept (jar assets
   kept archived, not deleted — see assets/graphics/sns-world-archive/ and
   SESSION_HANDOFF.md). Each SNS unit stacks its official brand mark
   above an index/name/arrow row — logo on top so it reads first, the
   row underneath doing the "which link is this" job. No cards, no
   vertical separators, no background art: five exits sitting directly
   on the section's own cream background, deliberately quieter than
   WORKS.

   Desktop (>=760px) is LOCKED: one row, five logos, 126px shared logo
   box height (object-fit:contain + each logo's own aspect-ratio scales
   it inside that box — this is why the five logos read as a matched
   set despite very different source-canvas shapes), horizontal spacing
   from .social-rail__link's own left/right padding (there's no
   explicit `gap`, so padding IS the inter-item spacing here).

   Mobile (<=759px) horizontal-scrolls the same list natively; the logo
   box drops to 126px * 0.88 = 110.88px (the approved Mobile scale) and
   .social-rail__item gets a min-width tuned so the next brand's mark —
   not just empty item padding — visibly peeks in from the right edge
   at rest (the GRAPHIC PEEK, ~15-25% of the next logo's own width).
   ========================================================= */
.social-rail {
  display: flex;
  list-style: none;
  margin: 0 auto;
  max-width: 1180px;
  padding: 0.4rem 0.2rem 0.6rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
}
@media (min-width: 760px) {
  .social-rail { overflow-x: visible; flex-wrap: wrap; justify-content: center; }
}
/* PHASE R2-A ADJUSTMENT (760–1179px only): R2-A's Desktop description
   text widened every unit enough that plain width-based flex-wrap no
   longer reliably breaks after the 3rd item across this whole range —
   it produced an unwanted 4+1 split around 1024px, and the natural
   wrap point drifts with each item's actual rendered text width rather
   than staying at a fixed 3-item count. The standard flexbox "forced
   new row" technique fixes this without touching item widths, gaps, or
   padding: an invisible, zero-size pseudo-element with flex-basis:100%
   can never fit next to preceding content, so flex-wrap always starts
   a new line at it; `order` places it right after the 3rd real item
   (default order 0) and before the 4th/5th (order 2), so the row keeps
   exactly 3 items — X / note / YouTube — before wrapping, independent
   of text width, then Ameblo / LINE fall to their own centered row 2
   exactly as flex-wrap already centers any wrapped line. At >=1180px
   this rule no longer applies and the row reverts to fitting all 5 on
   one line, same as before this adjustment. */
@media (min-width: 760px) and (max-width: 1179px) {
  .social-rail::after {
    content: "";
    order: 1;
    flex-basis: 100%;
    width: 0;
    height: 0;
  }
  .social-rail__item:nth-child(n + 4) { order: 2; }
}
.social-rail::-webkit-scrollbar { height: 0; }

.social-rail__item {
  flex: 0 0 auto;
  scroll-snap-align: start;
}
/* LOOP SCROLL DELTA (Mobile only in practice — clones are only ever
   inserted when js/main.js's setupSocialLoopGallery runs on Mobile):
   boundary clones are a purely transient visual glimpsed for a moment
   before the scroll position is silently corrected onto the identical
   real logo at the other end of the rail, so — same as WORKS' loop
   gallery clones — they carry no interaction of their own. */
.social-rail__item--clone { pointer-events: none; }

.social-rail__link {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.9rem;
  min-height: 44px;
  /* horizontal padding is the ONLY source of inter-item spacing on
     Desktop (no `gap` on .social-rail) — 1.728rem = the approved +8%
     breathing-room pass over the original 1.6rem */
  padding: 1.4rem 1.728rem 1.2rem;
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}
.social-rail__logo {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  height: 126px;
  max-width: 260px;
}
.social-rail__logo-img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  object-fit: contain;
  display: block;
}
.social-rail__row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
}
/* CONTENT INFO DELTA: GAME STAGE SELECT framing — a short line under
   the service name saying what you'll find through the link, before
   opening it. Quieter than .social-rail__name (smaller, --ink-dim,
   not bold) so LOGO stays the clear visual lead and the name stays
   one step below that; no box/pill/speech-bubble, just type sitting
   in the existing card.
   PHASE R2-A (SNS PARITY): this rule used to sit inside the Mobile
   media query because js/main.js only rendered the description on
   Mobile; the R1 audit classified that as an unintended divergence
   (A-4 — Desktop showed less information for the same links), so the
   description now renders at every viewport with this same type
   treatment. Only the Mobile peek-card fade further down stays
   viewport-specific. */
.social-rail__desc {
  display: block;
  margin-top: -0.5rem;
  max-width: 220px;
  font-size: 0.74rem;
  line-height: 1.5;
  color: var(--ink-dim);
  text-align: center;
  /* TEXT RULES LOCK — the copy's own line structure wins over layout.
     nowrap (which .social-rail__link already sets, restated here so the
     intent is explicit) means the ONLY line breaks are the <br>s
     js/main.js renders from the "\n"s in data/social.json: each meaning
     unit — "オリジナルキャラクター" / "「はちきりん」のスタンプ" — stays
     whole instead of being re-wrapped mid-phrase at a narrow width.
     Nothing clamps this text either (no max-width, and the former
     Desktop `width:0; min-width:100%` rule is gone): the description
     now contributes its own min-content width, so a unit WIDENS to fit
     its copy rather than the copy re-flowing to fit the unit. Measured
     cost of that reversal: only the LINE unit grows (+4.4px), the row
     total stays inside the rail, and the count-based 3+2 forced break
     (further up) is width-independent by construction. */
  white-space: nowrap;
}
@media (max-width: 759px) {
  /* SELECT UI POLISH DELTA 02: logo scaled down again (110.88px * 0.82
     ≈ 91px) — with the description added under the name, the logo no
     longer needs to dominate the whole card; ICON/NAME/DESC now read as
     one unit. Everything else (each logo's own width) still follows
     proportionally via object-fit:contain. */
  .social-rail__logo { height: 91px; }
  /* SELECT DIAL layout: the active card is centered (scroll-snap-align
     below), with a previous- and next-card peek on either side instead
     of only a right-side peek. flex-basis:70% (no extra rail padding
     needed — the two boundary clones already provide scroll room past
     both real ends) leaves the remaining 30% split 15%/15% on either
     side of a centered card. */
  .social-rail__item { flex: 0 0 70%; scroll-snap-align: center; }

  /* PEEK CONTENT CLEANUP: the description (type rule above, shared with
     Desktop since PHASE R2-A) is hidden by default here — a left/right
     peek card only ever showed a mid-sentence fragment of its
     description ("おち…", "書斎…"), pure noise since it can't be read
     anyway at that width. Only the active (centered) card's description
     shows, via js/main.js's .social-rail__item--active toggle.
     Logo/index/name stay visible on peek cards either way — "there's
     another SNS over there" without inviting anyone to read it.
     Mobile-only by nature: on Desktop all five units are on screen at
     once, so there is no peek card and nothing to hide. */
  .social-rail__desc {
    opacity: 0;
    transition: opacity 0.25s ease;
  }
  .social-rail__item--active .social-rail__desc { opacity: 1; }
}

/* ---------------------------------------------------------
   SNS WORLD — GUIDE UI (X first). A guided item (js/main.js
   SOCIAL_LOGOS[icon].guide) keeps its brand mark, index / name row and
   copy exactly as before — the mark itself gets no glow and no tint —
   and adds one thing: the copy spoken by the owner — the round GUIDE
   avatar to the LEFT, a frosted speech bubble to its right, the tail
   pointing back at the avatar. The SNS's image colour is carried only
   by the avatar's ring (and, faintly, the bubble border). The palette
   is a per-accent block of custom properties on the link
   (data-accent="<icon>"); the structural rules below are shared, so the
   next SNS only needs its own palette block — the same avatar, the same
   bubble, a different ring.
   Static by design: no motion, so prefers-reduced-motion needs nothing.
   --------------------------------------------------------- */
/* X — WHITE / SILVER, the one achromatic category, and the only one whose
   colour has to be carried by tone alone. The official mark is the black;
   the guide is its white-and-silver world, so no hue of any kind is mixed
   in here — every value below is a neutral or a cool grey, never a tint
   of blue, and the black is never spread into the guide.
   FINAL TUNING: the silver was the weakest outline of the five by a wide
   margin. Measured against the section's cream, each accent's border
   reads at note 2.54:1, YouTube 3.56:1, LINE 3.09:1, Ameba 1.58:1 — and X
   at 1.34:1, which is why its outline dissolved while the other four held.
   The border now lands at 2.26:1 and the ring's outer edge a shade beyond
   it, between Ameba and note: unmistakably an outline, still unmistakably
   silver rather than charcoal (mid grey, L 0.34, nowhere near ink). The
   surface goes the other way to keep the contrast off the copy — plain
   white at the top of the gradient, the faintest cool white at the bottom,
   so it is a white card on cream, not a grey box.
   Density does the work, not size: ring widths, border width and every
   other measurement are untouched. The shadow gains a little ground
   contact (.06 -> .12 at the same 6px blur, plus a brighter inner white
   line) — enough to lift the card off the cream, and still far from a
   drop shadow or a glow. */
.social-rail__link[data-accent="x"] {
  /* GUIDE HIERARCHY v1 (Desktop preview): the ambient ripple's colour --
     cool silver / chrome-like light, the achromatic one. Deliberately low-alpha: at
     full opacity this should read as "light spreading", not "a ring". */
  /* PURE WHITE, not silver -- and deliberately at the same alpha as the
     other four, so white is never compensated into being the bright one. */
  --sns-ripple: rgba(255, 255, 255, 0.255);
  --sns-accent-ring: #ffffff;
  --sns-accent-ring-edge: rgba(138, 146, 157, 0.98);
  --sns-accent-border: rgba(148, 155, 164, 0.95);
  --sns-accent-bubble-hi: rgba(255, 255, 255, 1);
  --sns-accent-bubble-lo: rgba(246, 248, 251, 0.97);
  --sns-accent-bubble-tail: rgba(255, 255, 255, 0.99);
  --sns-accent-bubble-shadow: 0 1px 6px rgba(58, 46, 32, 0.12), inset 0 0 0 1px rgba(255, 255, 255, 0.6);
}
/* note — BLUE / PALE SKY BLUE (COLOR REBALANCE; the dusty rose this
   block used to carry is retired). Built like the three after it: the
   colour is in the outline, the inside is white with only a breath of
   blue in it. The border blue (rgb(88,146,203), hue ~211) is a soft,
   slightly greyed sky blue — deliberately not cyan, not navy, and not a
   saturated pastel, so it reads as blue at a glance without turning the
   card childish; the interior does the opposite job and stays almost
   white so the copy keeps its ink. No glow, no blue fill, and the note
   mark stays black.
   The portrait is the blue one supplied for this category — sky, sea and
   a blue cardigan, framed wide on purpose so the world around him reads
   at 40-52px, not just a face. Like Ameba's, it arrives with its own
   rings (a white band, then a light blue one), so the CSS ring layers
   are switched off here and only the series' soft ground shadow remains
   — no white-blue-white-blue stack. Nothing in the picture is touched:
   no crop onto the face, no colour correction, no filter. */
.social-rail__link[data-accent="note"] {
  /* GUIDE HIERARCHY v1 (Desktop preview): the ambient ripple's colour --
     dusty rose, pulled well off note's own blue-green. Deliberately low-alpha: at
     full opacity this should read as "light spreading", not "a ring". */
  /* note's ripple is BLUE, not the earlier dusty rose, picked at the bluest
     deeper and more chromatic than it looks in isolation, ON PURPOSE: the
     cream ground has B 208 against R 245, so at these low alphas a mid-tone
     blue composites almost neutral -- the blue read has to come from the
     source being far enough from the ground, not from opacity. The
     intensity hierarchy fixed alpha here, so hue was the only lever left,
     and this source is pushed nearly as far as the maths allows.

     THE CEILING, worth writing down so nobody re-tunes this hoping for
     more. Composited over #f5e9d0, B-R = -37 + alpha * ((Bs-Rs) + 37).
     At alpha 0.1548 a B-R of +15 would need a source B-R of 299, which
     does not exist -- even pure rgb(0,0,255) only reaches +8.2, and this
     source reached +6.7, i.e. 82% of that ceiling.
     A later round then dimmed note ALONE (it sat slightly forward of the
     other four) from 0.158 to 0.145, and some of that blue is the price:
     the composite is now rgb(212,211,215), B-R +3.1. Blue is still the
     highest channel and still above green, so the direction holds, but at
     this intensity the blue is genuinely faint. That is expected, and it
     is why the alpha reduction was deliberately NOT compensated by
     touching the hue -- more blue here needs more alpha, nothing else.
     CENTRE FILL, note only: the note mark has a lot of empty space inside
     it, and a hollow annulus on top of that read as a sparse area. This
     brand alone fills the core, at 35% of its own band (0.051 vs 0.145), so
     the light appears to be BORN at the centre and travel outward instead
     of arriving as a ring -- still translucent, never a solid disc, and far
     too faint to muddy the black mark. Its outer diameter follows the same
     global range as everyone else. */
  --sns-ripple: rgba(10, 80, 255, 0.145);
  --sns-ripple-core: rgba(10, 80, 255, 0.051);
  --sns-accent-ring: transparent;
  --sns-accent-ring-edge: transparent;
  --sns-accent-border: rgba(88, 146, 203, 0.92);
  --sns-accent-bubble-hi: rgba(252, 254, 255, 0.95);
  --sns-accent-bubble-lo: rgba(232, 242, 252, 0.88);
  --sns-accent-bubble-tail: rgba(246, 251, 255, 0.95);
  --sns-accent-bubble-shadow: 0 1px 6px rgba(28, 78, 128, 0.14), inset 0 0 0 1px rgba(88, 146, 203, 0.2);
  /* The finished composition — photo, white band and light blue ring —
     is a circle of 99.761% of this square master (1254 x 1254), centred
     at (49.841%, 49.841%); 100/99.761 plus the usual ~0.4% margin maps
     that circle onto the wrapper's, ring and all. One uniform scale: the
     master is square and its circle is round, so no per-axis value. */
  --guide-photo-scale: 100.64%;
  --guide-photo-x: 50.16%;
  --guide-photo-y: 50.16%;
}
/* YouTube — RED, carried by the outline rather than the fill. The red
   lives where it can be strong without getting in the way of reading:
   the bubble's edge, the tail that continues it, a faint inner line, a
   red-cast shadow and the avatar's outer ring. The inside stays white
   with only a whisper of red mixed in — paper with a drop of YouTube red
   — and the copy is the site's own ink, so the two lines read at 12:1
   instead of the 4.6:1 a filled red surface gave them. Distinct from
   note by construction: note is a greyed rose throughout with a soft
   rose edge, YouTube is white ringed by a vivid red. No glow — the brand
   mark supplies all the intensity this category needs, and stays the
   strongest thing on the card. */
.social-rail__link[data-accent="youtube"] {
  /* GUIDE HIERARCHY v1 (Desktop preview): the ambient ripple's colour --
     soft muted red, desaturated far from broadcast red. Deliberately low-alpha: at
     full opacity this should read as "light spreading", not "a ring". */
  --sns-ripple: rgba(224, 118, 110, 0.158);
  --sns-accent-ring: #ffffff;
  --sns-accent-ring-edge: rgba(211, 47, 34, 0.95);
  --sns-accent-border: rgba(226, 45, 32, 0.92);
  --sns-accent-bubble-hi: rgba(255, 250, 249, 0.95);
  --sns-accent-bubble-lo: rgba(255, 234, 229, 0.88);
  --sns-accent-bubble-tail: rgba(255, 246, 244, 0.95);
  /* red cast below, a hairline of the same red inside — weight for the
     outline without touching border-width, so the bubble's box is the
     same size it was */
  --sns-accent-bubble-shadow: 0 1px 6px rgba(180, 35, 25, 0.14), inset 0 0 0 1px rgba(226, 45, 32, 0.18);
  --guide-photo-scale: 109.3%;
  --guide-photo-x: 50.22%;
  --guide-photo-y: 50.35%;
}
/* YouTube's bubble is wide (its copy is the longest of the five) but its
   brand mark is the widest of the five too, so reserving 26px of the
   avatar keeps the guide inside the width the mark already gives the
   card: the card does not grow at all on Desktop and the five-across row
   is untouched. On Mobile the card is content-sized, so a smaller
   reservation there keeps it from swallowing the rail at 320px — the
   rest of the avatar hangs into the card's own side padding. */
.social-rail__link[data-accent="youtube"] .social-rail__guide { --guide-reserve: 2.5rem; }
/* Ameba — YELLOW, built like YouTube's red: a warm ivory inside behind a
   clearly yellow edge, never a yellow fill. The yellow is pushed a shade
   brighter and more saturated than the site's own honey gold so the BLOG
   category reads as its own colour rather than as more of the page's
   honey; a low-opacity honey-600 line just inside the border gives that
   bright yellow enough body not to dissolve into the cream ground. The
   Ameba mark stays green — brand green outside, category yellow in the
   guide. The avatar's rings come from the artwork itself (see below), so
   the CSS ring layers are switched off here and only the soft ground
   shadow of the series remains. */
.social-rail__link[data-accent="ameba"] {
  /* GUIDE HIERARCHY v1 (Desktop preview): the ambient ripple's colour --
     soft warm yellow, closest to the section's own honey. Deliberately low-alpha: at
     full opacity this should read as "light spreading", not "a ring". */
  --sns-ripple: rgba(236, 190, 100, 0.16);
  --sns-accent-ring: transparent;
  --sns-accent-ring-edge: transparent;
  --sns-accent-border: rgba(244, 175, 0, 0.95);
  --sns-accent-bubble-hi: rgba(255, 253, 247, 0.95);
  --sns-accent-bubble-lo: rgba(255, 246, 219, 0.88);
  --sns-accent-bubble-tail: rgba(255, 250, 235, 0.95);
  --sns-accent-bubble-shadow: 0 1px 6px rgba(150, 105, 20, 0.14), inset 0 0 0 1px rgba(198, 124, 26, 0.2);
  /* The artwork is a slightly wider-than-tall oval (1241 x 1209 of a
     1254 canvas). Scaled uniformly, its yellow frame gets clipped at 3
     and 9 o'clock; scaling each axis to its own measurement keeps the
     frame whole all the way round and stretches the picture by 2.6%,
     which is invisible at 40-52px. Nothing inside is cropped. */
  --guide-photo-scale: 101.4%;
  --guide-photo-scale-y: 104.1%;
  --guide-photo-x: 50.46%;
  --guide-photo-y: 51.62%;
}
/* LINE — GREEN, the last of the five. Same construction as YouTube's
   red: the colour lives in the outline, never in the fill. The ring is
   the series' own two layers, white inside and a deep vivid green
   outside — the white band is what keeps the deep green off the black
   striped suit in the photo, so neither edge dissolves into the other.
   The green (rgb(5,150,66)) is deliberately darker than the LINE mark's
   own #06C755: the official logo stays the brightest green on the card
   and the guide reads as its category, not as a second logo. Bubble
   interior is white with the faintest green cast; the copy stays ink.
   No glow. */
.social-rail__link[data-accent="line"] {
  /* GUIDE HIERARCHY v1 (Desktop preview): the ambient ripple's colour --
     soft green, muted well below LINE's signal green. Deliberately low-alpha: at
     full opacity this should read as "light spreading", not "a ring". */
  --sns-ripple: rgba(140, 202, 132, 0.158);
  --sns-accent-ring: #ffffff;
  --sns-accent-ring-edge: rgba(4, 138, 60, 0.96);
  --sns-accent-border: rgba(5, 150, 66, 0.95);
  --sns-accent-bubble-hi: rgba(250, 255, 251, 0.95);
  --sns-accent-bubble-lo: rgba(232, 248, 236, 0.88);
  --sns-accent-bubble-tail: rgba(244, 253, 246, 0.95);
  --sns-accent-bubble-shadow: 0 1px 6px rgba(8, 90, 42, 0.14), inset 0 0 0 1px rgba(5, 150, 66, 0.2);
  /* Unlike the other four masters this file is not square (1278 x 1230),
     so the two axes carry DIFFERENT numbers in order to keep the picture
     at its natural proportions — not to stretch it. The round picture
     inside spans 88.576% of the file's width and 91.220% of its height,
     centred at (49.804%, 49.228%); scaling each axis by 1/that plus the
     usual ~0.4% margin makes the artwork's own circle cover this
     wrapper's circle while the rendered pixel aspect stays 1.00004 of
     the original. Nothing is re-cropped: the supplied wide framing --
     head, upper body, striped suit, the outdoor planting behind -- is
     what the avatar shows. */
  --guide-photo-scale: 114.36%;
  --guide-photo-scale-y: 110.06%;
  --guide-photo-x: 50.22%;
  --guide-photo-y: 50.85%;
}
/* The unit is exactly as wide as avatar + bubble (fit-content, centred
   under the mark). The bubble is sized by its two-line copy (see
   .social-rail__guide-text), so the unit is ~159px on Mobile — wider
   than the 70% card's content box on a 320px screen — and is therefore
   allowed to spill into the link's own side padding (2 x 1.728rem),
   still inside the card, rather than squeezing the copy onto 3-4 lines. */
.social-rail__guide {
  --guide-avatar: 52px;
  --guide-gap: 0.6rem;
  /* How much of the avatar's footprint counts toward the card's width.
     The whole of it by default (the avatar then sits fully inside the
     card, as X's does); a guided SNS whose copy makes a wide bubble can
     reserve less and let the rest hang into the card's own side padding,
     which is what note does on Desktop — see the override below. The
     bubble stays on the card's centre axis either way: whatever is
     reserved is taken back by the translate. */
  --guide-reserve: calc(var(--guide-avatar) + var(--guide-gap));
  display: flex;
  align-items: center;
  gap: var(--guide-gap);
  width: max-content; /* fit-content would clamp to the content box and squeeze the bubble */
  max-width: calc(100% + 2 * 1.728rem);
  margin-top: -0.3rem;
  text-align: left;
  white-space: normal;
  /* POSITION BALANCE — what sits on the card's centre axis is the BUBBLE,
     not the avatar+bubble row. Centring the row put the bubble half an
     avatar-plus-gap to the right of the brand mark and the "01 X" line,
     which read as a right-leaning column on a real device. Pulling the
     row left by exactly that half puts mark, name and bubble on one
     vertical axis and leaves the avatar beside it, where the tail still
     points. transform, not margin, so this is purely positional: no card
     widens, no intrinsic width changes, the Mobile card rhythm is
     untouched. */
  /* --guide-shift is an optical nudge applied to the WHOLE group, so the
     avatar/bubble relationship and the tail can never drift apart; 0 for
     everyone unless a column asks for it (LINE does, on Desktop). */
  transform: translateX(calc(-0.5 * var(--guide-reserve) + var(--guide-shift, 0px)));
}
.social-rail__guide-avatar {
  position: relative;
  flex: 0 0 auto;
  /* size unchanged (52px Mobile / 40px Desktop) — read from the variable
     so the axis correction above stays exact if it is ever re-tuned */
  width: var(--guide-avatar);
  height: var(--guide-avatar);
  /* 0 while the whole footprint is reserved; negative when only part of
     it is, which pulls the avatar out of the guide's measured width
     without moving it relative to the bubble */
  margin-left: calc(var(--guide-reserve) - var(--guide-avatar) - var(--guide-gap));
  border-radius: 50%;
  overflow: hidden;
  /* a 2px white ring with a 1px silver edge and a soft ground shadow —
     thin, clean, a little presence, no glow; the photo is untouched */
  box-shadow:
    0 0 0 2px var(--sns-accent-ring),
    0 0 0 3px var(--sns-accent-ring-edge),
    0 2px 6px rgba(58, 46, 32, 0.12);
}
/* The supplied master is a round portrait centred on a transparent
   canvas whose circle spans ~88.5% x 91% of it (alpha-measured, not
   guessed). Scaling the image by 1 / that ratio and centring it makes
   the artwork's own edge coincide with this wrapper's circle, so the
   ring hugs the photo instead of floating around a transparent margin.
   The file is displayed as-is — nothing is cropped off the portrait. */
.social-rail__guide-avatar img {
  /* The supplied master is a photo already composed as a round picture
     on a white square canvas (X: the white-suit / teacup photo,
     1254x1254). The job here is only to line that finished circle up
     with this wrapper's circle — the site's circle IS the photo's own
     circle, whole composition intact. It is never re-cropped: no zoom
     onto the face, no re-framing for legibility at 40-52px; the avatar
     is a small picture, not a portrait shot.
     Each guided SNS has its own photo, so each carries its own three
     numbers in its palette block above; the fallbacks here are X's.
     They are read off the file: for a painted circle centred at
     (cx, cy) spanning d of the canvas, the uniform scale is 100/d plus
     ~0.4% of margin (so the white surround can never bleed in past a
     pixel of measurement error, at the cost of the outermost ~0.2% of
     the picture's radius), and left/top = 50 - (c-50)*scale place the
     picture's centre on the wrapper's centre.
       X    circle (50.12%, 49.96%), d 98.80/99.12%  -> 101.6% / 49.88% / 50.04%
       note circle (49.92%, 49.88%), d 99.52/99.28%  -> 101.1% / 50.08% / 50.12% */
  position: absolute;
  left: var(--guide-photo-x, 49.88%);
  top: var(--guide-photo-y, 50.04%);
  width: var(--guide-photo-scale, 101.6%);
  /* per-axis only where the artwork needs it (Ameba); everyone else
     falls back to the single uniform scale */
  height: var(--guide-photo-scale-y, var(--guide-photo-scale, 101.6%));
  max-width: none;
  transform: translate(-50%, -50%);
  display: block;
}
.social-rail__guide-bubble {
  position: relative;
  flex: 0 1 auto; /* shrink-to-fit around the two-line copy at every width */
  min-width: 0;
  padding: 0.5rem 0.6rem 0.45rem;
  border-radius: 12px;
  /* milky, faintly translucent; frosted where there is anything behind
     it (the Mobile SNS WORLD art), plain milk on the Desktop cream */
  background: linear-gradient(160deg, var(--sns-accent-bubble-hi, rgba(255, 255, 255, 0.82)), var(--sns-accent-bubble-lo, rgba(255, 252, 246, 0.62)));
  border: 1px solid var(--sns-accent-border);
  box-shadow: var(--sns-accent-bubble-shadow, 0 1px 6px rgba(58, 46, 32, 0.06), inset 0 0 0 1px rgba(255, 255, 255, 0.45));
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
/* tail — always toward the avatar on the left */
.social-rail__guide-bubble::before {
  content: "";
  position: absolute;
  left: -4px;
  top: 50%;
  width: 9px;
  height: 9px;
  transform: translateY(-50%) rotate(45deg);
  background: var(--sns-accent-bubble-tail, rgba(255, 255, 255, 0.92));
  border-left: 1px solid var(--sns-accent-border);
  border-bottom: 1px solid var(--sns-accent-border);
  border-bottom-left-radius: 2px;
}
/* a short honey-gold hairline in the bottom-right corner — the one small
   touch of the site's own gold, inside the bubble's padding */
.social-rail__guide-bubble::after {
  content: "";
  position: absolute;
  right: 7px;
  bottom: 4px;
  width: 12px;
  height: 1px;
  background: linear-gradient(90deg, rgba(198, 124, 26, 0), rgba(198, 124, 26, 0.55));
}
.social-rail__guide-text {
  display: block;
  max-width: 6.4em; /* X only: one sentence, no explicit break — clamped to the approved "お仕事情報を / 中心に投稿" two-line shape at every width */
  margin: 0 auto;
  font-size: 0.74rem;
  line-height: 1.5;
  color: var(--sns-accent-text, var(--ink));
  text-align: center;
  white-space: normal;
}
/* TEXT RULES LOCK — guide copy that carries explicit breaks (note's
   "書斎です。/ 脳内を少しだけ覗けます", and YouTube / LINE once they are
   guided) shows exactly those lines: no clamp, no re-wrap, each meaning
   unit whole on one line and the bubble widened to fit it. js/main.js
   adds this modifier whenever the copy contains a "\n". It has to sit
   AFTER the rule above: both are single-class selectors, so the later
   one wins — placed before it, the 6.4em clamp and `normal` silently
   beat it (which is what happened to note's copy at first). */
.social-rail__guide-text--fixed {
  max-width: none;
  white-space: nowrap;
}
@media (min-width: 760px) {
  /* Desktop: a compact unit inside the narrower row items — smaller
     avatar, bubble shrink-to-fit around the two-line copy */
  .social-rail__guide { --guide-avatar: 40px; --guide-gap: 0.5rem; }
  /* note's locked copy needs a ~152px bubble — half again as wide as X's.
     Reserving the whole avatar on top of that would grow the note card by
     34px and push the five-across row past the rail at 1180px (measured:
     1133.2 against 1132). Reserving 28px of it instead keeps the row at
     ~1113px, and the remaining ~6px of avatar hangs into the card's own
     side padding — the same thing X's avatar does at 320px. Mobile keeps
     the full reservation: the rail scrolls there, so the card can simply
     be wider. */
  .social-rail__link[data-accent="note"] .social-rail__guide { --guide-reserve: 28px; }
  .social-rail__link[data-accent="youtube"] .social-rail__guide { --guide-reserve: 26px; }
  /* X is the one card whose width is set by its guide rather than its name
     row (content = reserve + bubble), so it is the only one that grows when
     the avatar does. At the v2 54px avatar the default full reservation
     (54 + 8 = 62px) took the five-across total to 1130.7px against the
     rail's 1125.6px content box and wrapped the row onto two lines.
     Reserving 44px instead lets the remaining 18px of avatar hang into the
     card's own 27.6px side padding — the same thing note, YouTube and LINE
     already do, and a smaller hang than either note or YouTube. Nothing
     about the avatar or the bubble changes: the size is untouched and the
     bubble stays on the card's centre axis, since whatever is reserved is
     taken back by the guide's own translate. */
  .social-rail__link[data-accent="x"] .social-rail__guide { --guide-reserve: 44px; }
  /* LINE has the longest copy of the five and the second-shortest name
     row, so its bubble (163.6px) is 26px wider than "05 LINEスタンプ ↗"
     (137.6px). Reserving any of the avatar on top of that grows the card
     enough to push the five-across row past the rail at 1180px (measured:
     1132.8 against the rail's 1125.6 content box even with zero reserved).
     A negative reserve discounts the rest: at -36px the unit measures
     127.6px — 10px UNDER the name row — so the LINE card is sized by its
     name row, exactly as an unguided card is, and the row total drops from
     1113.2px to 1106.8px, i.e. further inside the rail than it is today.
     Nothing about the bubble changes: it keeps the series' padding and its
     locked two lines, and it still sits whole inside the card (14.7px of
     the card's own padding to spare on each side) and on the card's centre
     axis. Only the avatar hangs further out to the left than note's or
     YouTube's — the element the POSITION BALANCE round put last. Mobile
     keeps the full reservation, as for the other four. */
  .social-rail__link[data-accent="line"] .social-rail__guide { --guide-reserve: -36px; }
  /* LINE's -36px reserve pulls its avatar further left than any other
     column's, which read as the guide group sitting off to the left of
     the LINE mark. A +16px optical nudge on the group only -- reserve,
     sizes and the avatar/bubble relationship are untouched, and because
     this is a transform it changes nothing about the card's measured
     width, so the five-across row is unaffected. Desktop only; the other
     four columns keep --guide-shift at its 0 default. */
  .social-rail__link[data-accent="line"] .social-rail__guide { --guide-shift: 16px; }

  /* ===== SNS GUIDE VISUAL HIERARCHY v1 — DESKTOP VISUAL TEST =====
     Goal: the owner's own avatar + speech bubble should read at least as
     strongly as the official brand mark, so the row says "Ayagiri is
     introducing each SNS" rather than "five logos". The brand marks stay
     the navigation entrance -- quieted, never demoted to small buttons.
     Desktop-scoped on purpose: Mobile is unchanged this round. */

  /* OFFICIAL MARK -> ~88%. Scaled with a transform rather than by
     shrinking .social-rail__logo's 126px box, for two reasons: the box
     keeps its height so the link's hit area, the card height and the
     section height are all literally unchanged; and a transform scales
     whatever size each logo actually resolved to, so every brand's own
     optical correction (they are height- or width-bound depending on the
     source canvas) is preserved instead of being flattened to one px.
     transform-origin keeps the marks on their existing bottom baseline. */
  .social-rail__logo-img {
    transform: scale(0.88);
    transform-origin: bottom center;
  }

  /* AYAGIRI AVATAR -> v2: 54px (40px originally, 48px in v1). Enough that
     the owner reads as a guide character speaking, not a small profile
     icon beside a label. The ring/edge/shadow live on this element as
     fixed-px box-shadows, so they follow the avatar without the halo
     itself inflating -- deliberately left restrained at this size. Photo
     asset, framing and the per-brand photo-centring numbers untouched. */
  .social-rail__guide { --guide-avatar: 54px; }

  /* BUBBLE -> v2: another ~6.5% of breathing room on top of v1, to stay
     in balance with the 54px avatar (0.42/0.55/0.4rem originally,
     0.47/0.62/0.45 in v1). Still bought with padding only: font-size,
     copy and line breaks are untouched, and the gradient, border,
     radius, tail, frosted blur and honey-gold hairline are all as they
     were -- the bubble gains room, not weight. */
  .social-rail__guide-bubble { padding: 0.50rem 0.66rem 0.48rem; }
}

/* ===== ORGANIC LIGHT RIPPLE =====
   A very faint wash of light breathing outward from behind each brand
   mark. Ambient, not a beacon: low alpha, a slow 5-8s cycle, and two
   layers at most. Pure CSS on the existing element's own pseudo-
   elements -- no new DOM, no SVG, no canvas, no library.
   ORGANIC, not a machine circle: each layer uses a four-value
   border-radius that is not a perfect circle and animates to a
   different one, and scales slightly more on one axis than the other,
   so the edge wobbles as it expands instead of reading as a radar
   sweep. The two layers use different radii and opposite asymmetry.
   Behind the mark and inert: z-index 0 under the img's own stacking,
   pointer-events:none, so link hit area and logo readability are
   untouched. */
.social-rail__logo {
  position: relative;
  isolation: isolate; /* keeps the ripple's z-index local to this card */
}
.social-rail__logo-img { position: relative; z-index: 1; }
.social-rail__logo::before,
.social-rail__logo::after {
  /* Peak opacity is a variable so a breakpoint can tune the wash without
     duplicating the animation.
     AMPLIFICATION: reported invisible twice. The root cause was not only
     intensity -- the old envelope put peak opacity at 22% of the timeline,
     when the ring had reached barely 0.83 scale, so the wave was brightest
     while it was SMALLEST and had already faded by the time it was wide.
     The envelope now rises early and HOLDS through the expansion (see the
     keyframes), and these peaks pair with roughly doubled brand alphas for
     an effective peak around 0.5. */
  --ripple-peak: 0.98;
  --ripple-peak-alt: 0.84;
  content: "";
  position: absolute;
  left: 50%;
  top: 55%;
  /* The base box is LARGER than the mark it sits behind (the Desktop logo
     box is ~112x126), so the wave has somewhere to travel instead of dying
     inside the logo's own footprint.

     RANGE: the base sizes stay put and only the END SCALE moves, because
     the gradient's band percentages are relative to this element -- keeping
     the base keeps the band's thickness proportional at any diameter, so
     shortening the travel does not also thin the wave out. Start scales are
     unchanged too, so the wave still emerges from behind the mark:
       L1  Ø48 -> Ø138 (scale 0.32 -> 0.920), outward travel 62.7 -> 45.0px
       L2  Ø57.8 -> Ø160 (scale 0.34 -> 0.941), outward travel 72.7 -> 51.1px
     (-28% / -30% on this round's travel). An end scale below 1 is fine and
     is not a shrink: each layer still expands ~2.8x from its start, and
     both clear the mark they sit behind (Desktop 112x126, Mobile 81x91).
     Envelope, timing and delays remain untouched; intensity is tuned only
     through each brand's own --sns-ripple alpha, never the envelope. */
  width: 150px;
  height: 150px;
  margin: -75px 0 0 -75px;
  z-index: 0;
  pointer-events: none;
  /* A BROAD band with volume, not a thin ring: transparent core, colour
     rising from 32%, held at full strength across 52-70%, then a long soft
     falloff to 88%. Never a hard 1-2px outline. */
  background: radial-gradient(circle,
    var(--sns-ripple-core, transparent) 0%,
    var(--sns-ripple-core, transparent) 32%,
    var(--sns-ripple, rgba(232, 169, 59, 0.22)) 52%,
    var(--sns-ripple, rgba(232, 169, 59, 0.22)) 70%,
    transparent 88%);
  border-radius: 48% 52% 55% 45%;
  opacity: 0;
  animation: snsGuideRipple 7s ease-out infinite;
}
.social-rail__logo::after {
  /* the second wave: larger, softer, later */
  width: 170px;
  height: 170px;
  margin: -85px 0 0 -85px;
  border-radius: 56% 44% 47% 53%;
  animation: snsGuideRippleAlt 8.5s ease-out infinite;
  animation-delay: 2.6s;
}
/* DESYNCHRONISED by fixed per-brand delays -- no JS, no randomness.
   Spread across the 6.4s cycle so the five never bloom together; the
   row should read as quiet breathing, not five simultaneous pulses. */
.social-rail__link[data-accent="x"] .social-rail__logo::before { animation-delay: 0s; }
.social-rail__link[data-accent="note"] .social-rail__logo::before { animation-delay: 1.3s; }
.social-rail__link[data-accent="youtube"] .social-rail__logo::before { animation-delay: 2.7s; }
.social-rail__link[data-accent="ameba"] .social-rail__logo::before { animation-delay: 4.1s; }
.social-rail__link[data-accent="line"] .social-rail__logo::before { animation-delay: 5.2s; }
.social-rail__link[data-accent="x"] .social-rail__logo::after { animation-delay: 3.4s; }
.social-rail__link[data-accent="note"] .social-rail__logo::after { animation-delay: 5.0s; }
.social-rail__link[data-accent="youtube"] .social-rail__logo::after { animation-delay: 0.9s; }
.social-rail__link[data-accent="ameba"] .social-rail__logo::after { animation-delay: 6.3s; }
.social-rail__link[data-accent="line"] .social-rail__logo::after { animation-delay: 2.1s; }

/* Hover / keyboard focus lift the ambient wash a little. Deliberately
   only opacity -- no extra scale-up, bounce, flash or hard glow; the
   existing focus-visible outline and link behaviour are untouched. */
.social-rail__link:hover .social-rail__logo::before,
.social-rail__link:hover .social-rail__logo::after,
.social-rail__link:focus-visible .social-rail__logo::before,
.social-rail__link:focus-visible .social-rail__logo::after {
  animation-duration: 5.8s;
}

@media (prefers-reduced-motion: reduce) {
  /* Ambient motion is the whole point of the ripple, so under reduced
     motion it stops expanding entirely. What is left is one static,
     even fainter halo behind each mark -- enough that the brand still
     sits in a little of its own light, with nothing moving. The links
     themselves are untouched. */
  .social-rail__logo::before {
    animation: none;
    /* the animated peak is now ~0.5 effective; the STATIC halo deliberately
       does not inherit that -- 0.4 over a ~0.5 alpha lands near 0.21
       effective, the same quiet presence it had before this round */
    opacity: 0.4;
    transform: scale(1.5);
  }
  .social-rail__logo::after { animation: none; opacity: 0; }
}
@keyframes snsGuideRipple {
  0% { transform: scale(0.32) rotate(0deg); border-radius: 48% 52% 55% 45%; opacity: 0; }
  14% { opacity: var(--ripple-peak, 0.5); }
  58% { opacity: calc(var(--ripple-peak, 0.5) * 0.72); }
  100% { transform: scale(0.92) rotate(8deg); border-radius: 55% 45% 46% 54%; opacity: 0; }
}
@keyframes snsGuideRippleAlt {
  0% { transform: scale(0.34) rotate(0deg); border-radius: 56% 44% 47% 53%; opacity: 0; }
  18% { opacity: var(--ripple-peak-alt, 0.38); }
  62% { opacity: calc(var(--ripple-peak-alt, 0.38) * 0.7); }
  100% { transform: scale(0.941) rotate(-7deg); border-radius: 44% 56% 53% 47%; opacity: 0; }
}
@media (max-width: 759px) {
  /* ===== SNS GUIDE VISUAL HIERARCHY — MOBILE ADAPTATION v1 =====
     The same shift Desktop got: Ayagiri's avatar and bubble come forward,
     the official mark steps back a little. Values are NOT copied from
     Desktop — Mobile starts from its own base (91px logo box, 52px
     avatar) and the card is a scroll-snapped 70%-width peek card, so
     everything here is tuned against that instead. */

  /* OFFICIAL MARK -> 88% of its Mobile size, the same factor Desktop
     uses. Scaled by transform, not by shrinking the 91px .social-rail__logo
     box, so the card height, the link's hit area and the rail's scroll
     geometry are all unchanged, and each brand keeps its own optical
     correction rather than being flattened to one px value. */
  .social-rail__logo-img {
    transform: scale(0.88);
    transform-origin: bottom center;
  }

  /* AVATAR v3: 58px -> 64px. Shrinking the official marks had made the
     whole guide read as small as well, which is backwards -- on Mobile the
     owner should be the lead and the mark the entrance above it. The guide
     is sized independently of the logo for exactly this reason. Ring,
     silver edge, halo design, asset and crop all untouched. */
  .social-rail__guide { --guide-avatar: 64px; }

  /* BUBBLE v3: ~1.2x overall presence, and unlike the earlier rounds the
     type moves too -- at Mobile sizes padding alone still left this
     reading as a small caption label. font-size 0.74 -> 0.82rem (11.84 ->
     13.12px, ~1.11x) and padding 0.545/0.655/0.49 -> 0.65/0.79/0.59rem.
     Copy and its line breaks are untouched, and .social-rail__guide-text's
     6.4em clamp is em-based, so X's approved two-line shape scales with
     the type rather than re-wrapping. */
  .social-rail__guide-bubble { padding: 0.65rem 0.79rem 0.59rem; }
  .social-rail__guide-text { font-size: 0.82rem; }

  /* RIPPLE: FULL PARITY with Desktop -- every Mobile reduction is dropped,
     since the report is that the wave cannot be seen at all. */

  /* same peek-card rule as the plain description: only the active
     (centred) card shows its guide */
  .social-rail__guide {
    opacity: 0;
    transition: opacity 0.25s ease;
  }
  .social-rail__item--active .social-rail__guide { opacity: 1; }
}
.social-rail__link:focus-visible {
  outline: 2px solid var(--honey-500);
  outline-offset: -2px;
}
.social-rail__index {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.05em;
  color: var(--ink-faint);
}
.social-rail__name {
  position: relative;
  font-family: var(--font-label);
  font-weight: 700;
  font-size: 0.92rem;
  color: var(--ink);
  padding-bottom: 0.2em;
}
/* underline grows in on hover/focus, rather than sitting there at rest —
   the digital-register equivalent of WORKS' text+underline tabs */
.social-rail__name::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--honey-500);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.28s ease;
}
.social-rail__link:hover .social-rail__name::after,
.social-rail__link:focus-visible .social-rail__name::after {
  transform: scaleX(1);
}
.social-rail__arrow {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--ink-dim);
  transition: transform 0.2s ease, color 0.2s ease;
}
.social-rail__link:hover .social-rail__arrow,
.social-rail__link:focus-visible .social-rail__arrow {
  transform: translate(2px, -2px);
  color: var(--honey-text);
}

/* SELECT UI POLISH DELTA 02: DIAL position indicator, same idea as
   WORKS' lever — a small fixed-width handle that rests at the rail's
   center and only deflects toward an edge while an active swipe is in
   progress (js/main.js updateSocialLever), not a growing 0-100% fill
   bar anchored to the left. Same track/handle colors as before
   (var(--border) rail, var(--honey-500) handle) — only the shape of
   the indicator's motion changed, not its palette. */
.social-rail__progress {
  max-width: 1180px;
  margin: 0.6rem auto 0;
  height: 2px;
  background: var(--border);
  border-radius: 999px;
  position: relative;
}
.social-rail__progress-thumb {
  position: absolute;
  top: 50%;
  left: 0;
  height: 100%;
  width: 18%;
  background: var(--honey-500);
  border-radius: 999px;
  transform: translateY(-50%);
}
@media (min-width: 760px) {
  .social-rail__progress { display: none; }
}

/* =========================================================
   SNS GUIDE — VERTICAL DIRECTORY v1 (DESKTOP STRUCTURE TEST)
   =========================================================
   Structural redesign of the Desktop SNS GUIDE, approved by User +
   ChatGPT: the horizontal five-card carousel is retired as the Desktop
   composition and replaced by a VERTICAL SNS DIRECTORY — five full-width
   rows stacked down the page, each row reading

       LEFT (~27%)   the official SNS identity: brand mark, then 01 / name / ↗
       RIGHT (~73%)  Ayagiri's avatar and his speech bubble for that SNS

   so the section reads as "Ayagiri guiding you through his five SNS",
   one entry at a time, instead of five equal cards competing for the
   same glance. STRUCTURE > EFFECT: the composition carries the meaning
   here, the ambient light is pulled right back (see the ripple override
   at the end of this block).

   SCOPE — deliberately CSS-only and deliberately inside this one
   `min-width: 760px` block:
   - js/main.js builds exactly the markup it always built. No new DOM, no
     new class, no new JS branch, so nothing about the data flow, the
     loop clones, the lever or the breakpoint resync changes.
   - Mobile (<= 759px) is not redesigned this round and cannot be reached
     from in here: every rule below is inside the Desktop query, and the
     Mobile rules (peek cards, loop clones, 91px mark, the dial) are
     untouched byte for byte.
   - The SNS PHOTO MARQUEE below the guide is a separate namespace
     (.sns-portfolio*) and is not referenced anywhere in this block.

   Where an earlier Desktop rule has to be undone rather than added to,
   it is undone HERE rather than edited in place, so the previous
   horizontal design stays readable in the file and this round is one
   contiguous, revertible block.
   ========================================================= */
@media (min-width: 760px) {
  /* --- the rail becomes a column, not a row ------------------------ */
  /* An editorial measure, not the full 1180px content width: at 1180 a
     directory row's right-hand guide ends up marooned from its brand
     mark. 760px keeps mark and bubble in one readable span while still
     leaving the rows clearly wider than tall, and keeps the trailing
     white space after the shortest bubble (X's) from reading as a void
     rather than as margin. */
  .social-rail {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: stretch;
    justify-content: flex-start;
    max-width: 760px;
    padding: 0;
    overflow-x: visible;
    scroll-snap-type: none;
  }
  /* The 760-1179px "forced 3+2 break" pseudo-element is a flex-basis:100%
     spacer, which in a COLUMN flex container would claim the container's
     full height instead of breaking a line. It has no job in a vertical
     directory, so it is switched off outright, and the `order: 2` that
     pushed items 4-5 past it goes back to the default so the five rows
     stay in data order 01-05. */
  .social-rail::after { display: none; }
  .social-rail__item:nth-child(n + 4) { order: 0; }

  /* --- one row per SNS -------------------------------------------- */
  .social-rail__item {
    flex: 0 0 auto;
    width: 100%;
    scroll-snap-align: none;
  }
  /* RESTRAINED DIVIDERS: a single hairline between rows, in the section's
     own border tone — no boxes, no cards, no per-row background, and
     nothing above the first row or below the last, so the directory
     reads as ruled type rather than as five containers. */
  .social-rail__item + .social-rail__item {
    border-top: 1px solid var(--border);
  }

  /* --- the row's two columns --------------------------------------- */
  /* The identity column is one grid column with two rows (mark above the
     01/name/↗ line); the guide spans both of those rows on the right, so
     it is vertically centred against the identity block as a whole
     rather than against either half of it. */
  .social-rail__link {
    display: grid;
    grid-template-columns: minmax(0, 27fr) minmax(0, 73fr);
    grid-template-rows: auto auto;
    grid-template-areas:
      "mark guide"
      "name guide";
    align-items: center;
    justify-items: start;
    column-gap: 2.6rem;
    row-gap: 0.75rem;
    /* generous vertical rhythm is what makes this read editorial; the
       horizontal design's 1.728rem side padding was inter-ITEM spacing
       and has no meaning once the items are stacked */
    padding: 1.9rem 0.4rem;
    min-height: 44px;
  }

  /* --- LEFT: the official SNS identity ----------------------------- */
  /* A shared box height again (the thing that made the five marks read as
     one matched set), just smaller than the horizontal design's 126px:
     in a row this wide the mark no longer has to hold a whole card on its
     own. Left-aligned rather than centred, so all five marks start on the
     same vertical rule down the page. */
  .social-rail__logo {
    grid-area: mark;
    height: 100px;
    max-width: 100%;
    align-items: center;
    justify-content: flex-start;
  }
  /* The horizontal design's 0.88 optical scale hung the marks off a
     bottom baseline shared with their neighbours. There are no
     neighbours on a row, so the mark simply fills its own box. */
  .social-rail__logo-img {
    transform: none;
  }
  .social-rail__row {
    grid-area: name;
    justify-content: flex-start;
  }

  /* --- RIGHT: Ayagiri's avatar + speech bubble --------------------- */
  /* Every per-brand --guide-reserve / --guide-shift value in the
     horizontal design existed for one reason: to stop a wide bubble from
     growing its card and breaking the five-across row. There is no
     five-across row any more and each guide has ~73% of a 900px column to
     sit in, so all of that is retired together — the avatar is reserved
     in full for all five, the compensating translate goes to zero, and
     every brand's guide starts on the same vertical rule. Written at the
     same (0,3,0) specificity as the per-accent overrides above — the
     `[data-accent]` is what matches their weight — and later in the file,
     so it wins without any of them being edited. Measured before the
     match: at (0,2,0) it silently lost, and LINE kept its -36px reserve
     and +16px nudge, putting its avatar 36px left of every other row's. */
  .social-rail__link[data-accent] .social-rail__guide {
    --guide-avatar: 96px;
    --guide-gap: 1.05rem;
    --guide-reserve: calc(var(--guide-avatar) + var(--guide-gap));
    --guide-shift: 0px;
    grid-area: guide;
    align-items: center;
    width: max-content;
    max-width: 100%;
    margin-top: 0;
    transform: none;
  }
  /* The avatar is the second lead of the row now (the brand mark is the
     first), so it steps up from 54px. Ring, edge and ground shadow are
     the series' own fixed-px box-shadows and are untouched — they follow
     the larger circle without becoming a halo. Photo assets and every
     per-brand photo-centring number are untouched. */
  .social-rail__guide-bubble {
    padding: 0.85rem 1.1rem 0.8rem;
    border-radius: 14px;
  }
  /* The copy and its locked line breaks are unchanged — this is the same
     two lines, set at a size that belongs to a 900px-wide row rather than
     to a 198px card. */
  .social-rail__guide-text {
    font-size: 1rem;
  }
  /* X is the one guide with no explicit break in its copy; its clamp is
     an em measure, so it follows the new size and keeps the approved
     "お仕事情報を / 中心に投稿" two-line shape. */

  /* --- STRUCTURE > EFFECT: the ambient ripple, pulled back --------- */
  /* The composition now says what the ripple was being asked to say, so
     the light is reduced to a single, much fainter layer: the second
     (larger, offset) layer is switched off entirely and the remaining
     one drops from a 0.98 peak to 0.42. No per-brand alpha is touched —
     each SNS keeps its own --sns-ripple hue and its own delay, just at
     well under half the presence. Desktop only; Mobile keeps both layers
     exactly as approved. */
  .social-rail__logo::before {
    --ripple-peak: 0.42;
  }
  .social-rail__logo::after {
    display: none;
  }
}
/* The reduced-motion path has to follow the same reduction, or it inverts:
   its static stand-in is a scale(1.5) wash at opacity 0.4, which was
   dimmer than the animated layer's 0.98 peak but is nearly the same
   strength as the new 0.42 one -- and at Ø225 it is far LARGER than
   anything the animation now reaches (Ø138 at its widest). A reader who
   asks for less motion would have got a bigger, brighter ripple than
   everyone else. Rescaled here to sit inside the animated layer's own
   range and at the same proportion of it: 0.4 -> 0.17 (the 0.42/0.98
   ratio applied to the static value) and scale 1.5 -> 0.85 (Ø127.5,
   just inside the animated maximum). Desktop only -- Mobile keeps the
   approved 0.4 / scale(1.5), which is still correct against Mobile's
   untouched 0.98 peak. */
@media (min-width: 760px) and (prefers-reduced-motion: reduce) {
  .social-rail__logo::before {
    opacity: 0.17;
    transform: scale(0.85);
  }
}

/* =========================================================
   SNS — EDITORIAL SOCIAL GALLERY
   =========================================================
   The SNS section rebuilt as a vertical gallery of five supplied banner
   visuals: 1 SNS = 1 work, read down the page the way a magazine spread
   is read. Image + whitespace + type + a quiet reveal, and nothing else --
   no cards, no coloured panels, no icons, no copy on top of the images
   (they carry their own). Cream / ivory / honey stays the ground.

   MEASURE. Every section on this site is a 1180px panel (.section
   max-width) with 1.5rem side padding; the gallery cancels that padding so
   the panel's full width is its measure, and stays INSIDE the panel rather
   than bleeding to the viewport (no other section does, and the brief's
   first priority after the images is staying one site). Desktop banners are
   therefore min(86vw, 1080px) inside 1180 -- the brief's min(82-88vw,
   1380px) starting point, adjusted to the panel -- which leaves 100px for
   the editorial offset: LEFT items start at the panel's left edge, RIGHT
   items end at its right edge (--sns-shift = 44px either side of centre),
   LINE takes the full panel. Tablet narrows the shift, Mobile drops it and
   runs banners nearly full-width with a 16px safe margin.

   RATIOS. Banners are 1774x887 (2:1) and the HACHIKIRIN slide 1522x1076;
   each <img> carries width/height so the box is reserved before the file
   arrives (no CLS) and the rendered box is always the file's own ratio --
   nothing is cropped at any width. The wide LINE slide is additionally
   capped by 78vh so a 1.41:1 picture never overruns a short viewport.

   MOTION reuses the existing reveal observer (js/main.js adds .is-visible
   once per .reveal element): the items simply carry `reveal` and this
   block restates the entrance with the gallery's own values.
   ========================================================= */
.sns-gallery {
  --sns-banner-w: min(86vw, 1080px);
  --sns-shift: 42px;
  /* COMPACT GALLERY RHYTHM: the inter-entry gap only (the flex `gap` on
     .sns-gallery__list). Reduced so the five artworks read as ONE
     continuous editorial collection rather than five isolated pieces --
     64px up to ~1280, then 72px at 1440 and 96px at 1920. History:
     clamp(112px, 12vw, 190px) -> clamp(100px, 10.4vw, 165px) -> this. */
  --sns-gap: clamp(64px, 5vw, 96px);
  --sns-radius: 8px;
  --sns-ambient: rgba(255, 255, 255, 0.5);
  position: relative;
  /* cancel .section's 1.5rem side padding: the panel is the measure */
  margin: clamp(40px, 4.5vw, 64px) -1.5rem 0; /* REFINEMENT 1: one-line intro (was clamp(48px, 6vw, 88px)) */
  padding-bottom: clamp(24px, 4vw, 64px);
  /* The ambient ellipse behind each piece runs 6% past the image on both
     sides. On a RIGHT-offset item at a viewport narrower than the panel
     (768-1228px) that extension leaves the viewport and becomes page
     overflow (measured 36px at 768, 25px at 1024). `clip` cuts it at the
     panel edge without turning the gallery into a scroll container, so the
     vertical reveal transform and the auto height are untouched; the
     ambient is already near transparent that far out. */
  overflow-x: clip;
}

/* ---- intro: the section's own head (SOCIAL MEDIA / SNS) stays the
   heading; under it one line, FOLLOW ME. -- a small editorial signpost in
   the same tracked label register as the eyebrow and the 01 / WORK / NEWS
   labels (REFINEMENT 1 replaced the earlier two-line Japanese statement).
   Quiet by construction: label face, small size, wide tracking, dim ink;
   not a button, no icon, no arrow, no motion of its own. ---- */
.sns-gallery__intro {
  max-width: 40rem;
  margin: 0 auto;
  padding: 0 1.5rem;
  text-align: center;
}
.sns-gallery__lead {
  font-family: var(--font-label);
  font-size: 0.82rem;
  font-weight: 500;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--ink-dim);
}

/* ---- the works ---- */
.sns-gallery__list {
  display: flex;
  flex-direction: column;
  gap: var(--sns-gap);
  /* PATCH 001 DELTA 02: the one-line intro (FOLLOW ME. -> SOCIAL MEDIA) moved
     into the section heading as its subtitle, so the intro -> list gap this
     margin provided has nothing to separate any more. 0; the head's own
     3rem margin-bottom plus .sns-gallery's margin-top now set the heading ->
     first artwork distance. (was clamp(52px, 6vw, 88px)) */
  margin-top: 0;
}
.sns-gallery__item {
  width: var(--sns-banner-w);
  margin-inline: auto;
}
/* an explicit left margin with `auto` on the right places the block; the
   max()/min() keep it inside the measure at any width */
.sns-gallery__item--left {
  margin-left: max(0px, calc((100% - var(--sns-banner-w)) / 2 - var(--sns-shift)));
}
.sns-gallery__item--right {
  margin-left: min(calc(100% - var(--sns-banner-w)), calc((100% - var(--sns-banner-w)) / 2 + var(--sns-shift)));
}
/* The closing piece takes the full panel, and its width no longer depends on
   the viewport HEIGHT: the previous calc(78vh * 1.4145) term was a height
   ceiling expressed as a width, so a short window shrank the artwork sideways. */
.sns-gallery__item--wide {
  width: min(100%, 1180px);
}


/* ---- link + image: the banner IS the piece. A small radius, a border of
   under 1px in weight (a 1px shadow ring at 5%), a shadow that is mostly
   negative spread so it reads as contact, not as a floating card. ---- */
.sns-gallery__link {
  display: block;
  position: relative;
  isolation: isolate; /* keeps the ambient's z-index:-1 inside this link */
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
/* ambient colour: air around the piece, never a surface. One soft ellipse
   behind the image in the SNS's own colour at a very low alpha, gone well
   before it could read as a background. */
.sns-gallery__link::before {
  content: "";
  position: absolute;
  left: -6%;
  right: -6%;
  top: -4%;
  bottom: -10%;
  z-index: -1;
  background: radial-gradient(ellipse at 50% 56%, var(--sns-ambient) 0%, transparent 68%);
  pointer-events: none;
}
/* Composited over the section's cream (245/233/208) the two cool colours
   need a source far from the ground or they land neutral -- the blue here
   composites to B-R +11, the green to G-R +8; the warm ones sit naturally. */
.sns-gallery__item[data-sns="x"]       { --sns-ambient: rgba(255, 255, 255, 0.6); }
.sns-gallery__item[data-sns="note"]    { --sns-ambient: rgba(30, 95, 235, 0.16); }
.sns-gallery__item[data-sns="youtube"] { --sns-ambient: rgba(228, 104, 92, 0.13); }
.sns-gallery__item[data-sns="ameba"]   { --sns-ambient: rgba(238, 186, 70, 0.17); }
.sns-gallery__item[data-sns="line"]    { --sns-ambient: rgba(60, 170, 90, 0.15); }

.sns-gallery__img {
  display: block;
  width: 100%;
  height: auto; /* width/height attributes fix the ratio; the box scales with the measure */
  border-radius: var(--sns-radius);
  background: var(--bg-soft);
  box-shadow:
    0 0 0 1px rgba(58, 46, 32, 0.05),
    0 1px 2px rgba(58, 46, 32, 0.05),
    0 14px 36px -18px rgba(58, 46, 32, 0.22);
  transition: transform 0.55s cubic-bezier(0.22, 0.61, 0.36, 1), box-shadow 0.55s ease;
}
/* FOCUS RING, DRAWN INSIDE THE ARTWORK AT EVERY WIDTH.
   It used to sit 6px outside the link, so with the 2px stroke the indicator
   occupied the 8px band beyond the banner — and .sns-gallery is overflow-x:
   clip (deliberately, to stop the ambient ellipse becoming page overflow), so
   any banner within 8px of the panel edge lost that side of its ring.
   Measured: side banners lost 8.00px at 760 and 5.6px at 800, and the LINE
   banner, which spans the full panel by design, lost 8.00px on BOTH sides at
   every width from 760 up — 1024 and 1440 included. Confirmed in pixels, not
   only in geometry: the ring simply was not painted there.
   Any positive offset has the same fate on a full-width banner, so the ring
   has to come inside. -3px is not a new idea: it is the value the <=759 rule
   below has always used for exactly this reason, and it puts the whole 2px
   stroke 1px clear of the border box. border-radius follows the image's own
   --sns-radius now that the ring hugs the artwork instead of floating outside
   it, replacing the 10px that suited the old outer position.
   Still an `outline`, so forced-colors still re-colours a real focus
   indicator; still --honey-text-deep, so the approved colour is unchanged;
   and nothing is drawn unless :focus-visible matches, so the normal state is
   untouched. */
.sns-gallery__link:focus-visible {
  outline: 2px solid var(--honey-text-deep);
  outline-offset: -4px;
  border-radius: var(--sns-radius);
}
/* ...and once it is inside, one colour is not enough. Out on the cream ground
   the deep amber held 4.92:1 wherever it was not clipped; sitting on the
   artwork it reaches 3:1 on only 64% of its perimeter, worst 1.01 -- measured
   across all five banners at 390/760/1024/1440. That is the same thing a
   single hue does over any photograph, and the same thing BATCH 2D measured
   on the WORKS posters, so this reuses the answer approved there rather than
   inventing one: --surface over --ink, the palette's existing extremes, no
   new token.
   Geometry follows that batch too. The pair only helps if BOTH bands have
   artwork to contrast against, so the overlay is held 2px in and 2px of
   artwork is left outside it: --surface occupies 2..4px, --ink 4..6px, 4px of
   ink in total, far enough inside that the gallery's clip can never reach it.
   The outline above is offset -4px so it occupies exactly the 2..4px band the
   --surface border covers -- invisible in normal rendering, but forced-colors
   drops box-shadow and re-colours the outline, so a real system-adjustable
   focus indicator is still there.
   ::before on this link is the ambient ellipse, so the overlay is ::after;
   the link is `isolation: isolate`, which keeps both inside it. Nothing is
   generated unless :focus-visible matches, so the normal state is untouched,
   and pointer-events:none keeps the hit area exactly as it was. */
.sns-gallery__link:focus-visible::after {
  content: "";
  position: absolute;
  inset: 2px;
  border: 2px solid var(--surface);
  box-shadow: inset 0 0 0 2px var(--ink);
  border-radius: calc(var(--sns-radius) - 2px);
  pointer-events: none;
}
/* pointer devices only: the piece lifts by just over a percent and the
   shadow deepens a little. No overlay, no button, no label. */
@media (hover: hover) and (pointer: fine) {
  .sns-gallery__link:hover .sns-gallery__img {
    transform: scale(1.012);
    box-shadow:
      0 0 0 1px rgba(58, 46, 32, 0.06),
      0 2px 4px rgba(58, 46, 32, 0.06),
      0 22px 48px -20px rgba(58, 46, 32, 0.3);
  }
}

/* ---- SOFT EDITORIAL REVEAL (motion UNFREEZE round; visual spec frozen) ----
   The ARTWORK emerges beneath a stable label: each banner starts a touch
   to the LEFT and slightly low, faint and very slightly soft, and resolves
   into place -- translate3d(-16px, 4px, 0) / opacity 0 / blur(2px) -> rest,
   900ms on the site's cubic-bezier(.22,.61,.36,1) ease-out. Not a slide:
   the travel is 16px, and opacity, movement and blur end together as one
   appearance.
   ARCHITECTURE. The item still carries the site's `reveal` class so the
   EXISTING observer (js/main.js revealObserver: fires once per element on
   entry, then unobserves) is the trigger -- per entry, play once, no
   replay -- but the item itself no longer moves or hides: the label stays
   put and the layout's FROZEN geometry (alternating offsets, --sns-gap) is
   never transformed. What animates is `.sns-gallery__frame`, a wrapper
   that owns ONLY the reveal transform, so the image's own hover
   `scale(1.012)` lives on a different element and the two can never
   overwrite each other. Transforms, opacity and filter only: document
   flow is untouched, CLS 0.
   NO-JS / JS-FAILURE SAFETY. The pre-reveal state is gated on
   `html.has-reveal-observer`, a class js/main.js adds only after the
   observer exists and is watching. Without it -- script blocked, failed
   or thrown before that line -- every banner is simply visible. ---- */
.sns-gallery__item.reveal,
.sns-gallery__item.reveal.is-visible {
  opacity: 1;
  transform: none;
  transition: none;
}
.sns-gallery__frame {
  display: block;
  transition:
    transform 0.9s cubic-bezier(0.22, 0.61, 0.36, 1),
    opacity 0.9s cubic-bezier(0.22, 0.61, 0.36, 1),
    filter 0.9s cubic-bezier(0.22, 0.61, 0.36, 1);
}
html.has-reveal-observer .sns-gallery__item.reveal:not(.is-visible) .sns-gallery__frame {
  opacity: 0;
  transform: translate3d(-16px, 4px, 0);
  filter: blur(2px);
}
/* DESKTOP DIRECTIONAL REVEAL (>=760px): each artwork emerges from the
   side of its own editorial offset -- the LEFT-placed X and YouTube from
   the left, the RIGHT-placed note and Ameba from the right, and LINE, the
   closing full-width piece, rises gently from below. Same 18px-class
   travel, same opacity / blur / 900ms / easing as before; only the vector
   differs, keyed on the data-sns attribute the markup already carries.
   Mobile (<=759px) keeps its approved -12px / +4px for all five (see the
   Mobile block below); the generic -16px above is the fallback for a
   width that matches neither rule, which today is none. */
@media (min-width: 760px) {
  html.has-reveal-observer .sns-gallery__item.reveal:not(.is-visible)[data-sns="x"] .sns-gallery__frame,
  html.has-reveal-observer .sns-gallery__item.reveal:not(.is-visible)[data-sns="youtube"] .sns-gallery__frame {
    transform: translate3d(-18px, 4px, 0);
  }
  html.has-reveal-observer .sns-gallery__item.reveal:not(.is-visible)[data-sns="note"] .sns-gallery__frame,
  html.has-reveal-observer .sns-gallery__item.reveal:not(.is-visible)[data-sns="ameba"] .sns-gallery__frame {
    transform: translate3d(18px, 4px, 0);
  }
  html.has-reveal-observer .sns-gallery__item.reveal:not(.is-visible)[data-sns="line"] .sns-gallery__frame {
    transform: translate3d(0, 16px, 0);
  }
}
/* the ambient wash follows the artwork it belongs to */
.sns-gallery__link::before { transition: opacity 0.9s cubic-bezier(0.22, 0.61, 0.36, 1); }
html.has-reveal-observer .sns-gallery__item.reveal:not(.is-visible) .sns-gallery__link::before { opacity: 0; }

/* ---- 05 LINE STAMP — HACHIKIRIN MINI EXHIBITION ----
   Native scroll-snap track. Each slide is the track minus the peek, so the
   next picture shows ~7% at the right edge and the row reads as continuing
   without any instruction. Vertical padding on the track gives the hover
   lift and the shadow room, since overflow-x:auto forces overflow-y to
   clip. Dots are 6px, honey when active, with a 24px invisible hit area. */
.sns-carousel {
  --peek: 7%;
  --slide-gap: 12px;
  position: relative;
}
.sns-carousel__track {
  position: relative;
  display: flex;
  gap: var(--slide-gap);
  list-style: none;
  margin: 0;
  padding: 10px 0 24px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  cursor: grab;
}
.sns-carousel__track::-webkit-scrollbar { height: 0; }
.sns-carousel__track.is-dragging {
  scroll-snap-type: none;
  scroll-behavior: auto;
  cursor: grabbing;
  user-select: none;
}
.sns-carousel__slide {
  flex: 0 0 calc(100% - var(--peek));
  scroll-snap-align: start;
}
.sns-carousel__slide .sns-gallery__link,
.sns-carousel__slide .sns-gallery__img { -webkit-user-drag: none; }
.sns-carousel.is-single .sns-carousel__slide { flex-basis: 100%; }
.sns-carousel.is-single .sns-carousel__track { cursor: default; overflow-x: hidden; }
.sns-carousel.is-single .sns-carousel__dots { display: none; }
.sns-carousel__dots {
  display: flex;
  justify-content: center;
  gap: 12px;
  margin-top: 2px;
}
.sns-carousel__dot {
  position: relative;
  width: 6px;
  height: 6px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--border);
  cursor: pointer;
  transition: background-color 0.3s ease, transform 0.3s ease;
}
.sns-carousel__dot::before {
  content: "";
  position: absolute;
  inset: -9px;
}
.sns-carousel__dot.is-active {
  background: var(--honey-500);
  transform: scale(1.3);
}
.sns-carousel__dot:focus-visible {
  outline: 2px solid var(--honey-500);
  outline-offset: 4px;
}

/* ---- breakpoints ---- */
@media (min-width: 760px) and (max-width: 1023px) {
  .sns-gallery {
    /* Keeps the banner width continuous across 760-1024: the line through the
       values this band must join, 760 -> 728px (the <=759 rule's last value)
       and 1024 -> 880.6px (= 86vw, the base rule's first). Was min(92vw,
       1080px), which shrank the banner as the viewport grew at both edges.
       A layout %, not vw, so both sides of 760 share one reference frame. */
    --sns-banner-w: calc(288px + 57.9%);
    --sns-shift: 22px;
  }
}
@media (max-width: 759px) {
  .sns-gallery {
    /* 16px safe margin either side (the section pads 24px) */
    margin-inline: -0.5rem;
    --sns-banner-w: 100%;
    --sns-shift: 0px;
    /* COMPACT GALLERY RHYTHM (96px -> 72px -> 48px). The distance BETWEEN
       entries only -- the flex `gap` on .sns-gallery__list, which never
       applies after the last item -- so the label-to-banner spacing, the
       intro spacing and LINE -> SNS PHOTO MARQUEE are all unaffected. */
    --sns-gap: 48px;
    --sns-radius: 6px;
  }
  .sns-gallery__intro { padding-inline: 0.5rem; }
  .sns-gallery__list { margin-top: 0; } /* PATCH 001 DELTA 02: intro is now the heading subtitle (was 48px) */
  /* REFINEMENT 1 -- EXPLICIT UNFREEZE (this round only): the legacy
     game-graphic layer (#sns::before, section-bg-06, opacity .58, set in
     the Mobile block near the top of this file) is switched off so the
     Mobile section reads as the same cream / ivory ground as Desktop and
     the banners are the only colour. Same selector, same Mobile query,
     later in source, so it wins with no specificity games; the original
     rule, the asset, background-size/position and Desktop are untouched --
     delete this one rule and the art is back. */
  #sns::before { display: none; }
  .sns-gallery__item--left,
  .sns-gallery__item--right { margin-inline: auto; }
  .sns-gallery__item--wide { width: 100%; }
  .sns-carousel { --peek: 8%; }
  /* the Mobile-only focus override that used to live here is gone: it set
     exactly this offset and radius, and the base rule now carries them at
     every width, so it was restating the base verbatim. The reasoning it
     recorded -- banners sit on the gallery's own edges, a ring drawn outside
     them is clipped away -- is the reasoning the base rule now gives, because
     it turned out to be true at every width, not only here. */
  /* reveal: same concept, slightly shorter travel on a small screen */
  html.has-reveal-observer .sns-gallery__item.reveal:not(.is-visible) .sns-gallery__frame {
    transform: translate3d(-12px, 4px, 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .sns-gallery__item.reveal { opacity: 1; transform: none; }
  /* no entrance movement, no entrance blur: the artwork is simply there */
  html.has-reveal-observer .sns-gallery__item.reveal:not(.is-visible) .sns-gallery__frame,
  html.has-reveal-observer .sns-gallery__item.reveal:not(.is-visible)[data-sns] .sns-gallery__frame,
  .sns-gallery__frame { opacity: 1; transform: none; filter: none; transition: none; }
  html.has-reveal-observer .sns-gallery__item.reveal:not(.is-visible) .sns-gallery__link::before { opacity: 1; }
  .sns-gallery__link:hover .sns-gallery__img { transform: none; }
  .sns-carousel__track { scroll-behavior: auto; }
  .sns-carousel__dot.is-active { transform: none; }
}

/* SNS PHOTO MARQUEE (preview, TEMPORARY UNFREEZE) — sibling of the
   PROFILE portfolio marquee, deliberately implemented as its OWN
   namespace + its OWN keyframes rather than sharing .profile-portfolio's
   rules: PROFILE's marquee is approved and must keep a visual diff of 0,
   so nothing here can reach it and a later change to either side cannot
   silently move the other. Same design language on purpose (fixed
   height + width:auto, no object-fit, 1rem gap, 6px radius, locally
   clipped strip, doubled sequence); the only intentional difference is
   speed -- 55s vs PROFILE's 60s, so SNS reads as the slightly more
   active everyday stream against PROFILE's calmer portfolio.

   Confined to the SNS section's own 1180px content column (the width
   .social-rail already uses) with overflow:hidden here rather than a
   full-bleed breakout, so the oversized track is clipped locally and can
   never push the document's scrollWidth past its clientWidth. */
.sns-portfolio {
  max-width: var(--max-w);
  /* SPACING: v2 raised this 3rem -> 4.5rem; v3 takes Mobile one small
     step further to 5rem (72px -> 80px measured guide-to-strip), purely
     for breathing room under the SNS links.

     It does NOT move the strip toward the background's LEVEL UP / EXP
     row, and is not intended to: that row is baked into the bottom ~4%
     of section-bg-06, which #sns::before paints with
     background-size:cover. Because the image's height is what `cover`
     matches here, the row always lands ~4% of the section height above
     the section's bottom edge, while the strip sits a fixed 96px above
     it (#sns padding-bottom). Growing this margin grows both together,
     so the offset between them is constant -- measured 57 / 57 / 58 /
     58px at 3 / 4.5 / 6 / 7.5rem. See the report for the full finding. */
  margin: 5rem auto 0;
}
@media (min-width: 760px) {
  /* Desktop has no game-graphic background (that block is Mobile-only),
     so this is breathing room alone: 3rem -> 4rem, +33%, matching a step
     already used elsewhere rather than a new number. Deliberately NOT the
     Mobile value -- there is no glyph band to reach here, and 6rem at
     Desktop would read as a gap between two sections. */
  .sns-portfolio { margin-top: 4rem; }
}
.sns-portfolio__strip {
  /* v2: a real horizontal scroller, not a clipped box. The automatic
     movement is driven by writing scrollLeft (js/main.js
     initSnsPhotoMarquee), so auto motion and the user's own drag act on
     the same property and cannot fight each other. touch-action is
     deliberately left at its default: the browser already pans this
     strip horizontally and scrolls the page vertically, and overriding
     it would only cost pinch-zoom. */
  overflow-x: auto;
  overflow-y: hidden;
  /* html sets scroll-behavior:smooth; be explicit that per-frame
     scrollLeft writes here must land instantly, never animate. */
  scroll-behavior: auto;
  /* keep a horizontal flick from chaining into page/browser back nav */
  overscroll-behavior-x: contain;
  /* same scrollbar treatment .social-rail already uses in this section */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.sns-portfolio__strip::-webkit-scrollbar { height: 0; }
.sns-portfolio__track {
  display: flex;
  align-items: center;
  width: max-content;
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 0;
  /* linear = constant speed; a marquee must never visibly accelerate or
     decelerate, least of all at the 10 -> 01 boundary. translateX(-50%)
     is exactly half of THIS track's own width and the track is exactly
     two 10-photo sequences wide, so -50% always lands on precisely one
     sequence at any viewport -- no per-viewport distance to hand-sync.

     v2: 55s -> 70s, a calmer drift. This keyframe animation is now only
     the NO-JS fallback -- when js/main.js is running it adds .is-manual
     and the movement is driven by scrollLeft instead, so that auto
     motion and the user's own scrolling share one property. */
  animation: snsPortfolioMarquee 70s linear infinite;
}
/* JS is present and driving the strip: drop the keyframes so the two
   mechanisms never both move the same track. */
.sns-portfolio.is-manual .sns-portfolio__track { animation: none; }
@keyframes snsPortfolioMarquee {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
.sns-portfolio__item { flex: 0 0 auto; }
.sns-portfolio__img {
  display: block;
  height: 280px;
  width: auto;
  /* no object-fit: these are supplied originals and each keeps its own
     landscape/portrait shape -- there is deliberately no box for any of
     them to be cropped into */
  border-radius: 6px;
}
@media (min-width: 760px) {
  .sns-portfolio__img { height: 260px; }
}
@media (prefers-reduced-motion: reduce) {
  /* AUTO MOTION STOPS, but all 10 real photos stay reachable: the strip
     is a native horizontal scroller either way now, and JS leaves auto
     movement switched off here, so what remains is a row the user moves
     entirely themselves. The loop's duplicate 10 -- meaningless once
     nothing is animating -- are hidden rather than left as an inert
     second copy to scroll through. */
  .sns-portfolio__track { animation: none; }
  .sns-portfolio__strip { -webkit-overflow-scrolling: touch; }
  .sns-portfolio__track li[aria-hidden="true"] { display: none; }
}

/* =========================================================
   CONTACT
   ========================================================= */
.contact__note {
  text-align: center;
  color: var(--ink-dim);
  max-width: 30em;
  margin: 0 auto 2.6rem;
  font-size: 0.9rem;
  line-height: 1.95;
}
.contact-form {
  max-width: 620px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 1.4rem;
  padding: 2.2rem 2rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 20px;
  box-shadow: var(--card-shadow);
}
@media (max-width: 560px) {
  .contact-form { padding: 1.7rem 1.2rem; }
}
.form-row { display: flex; flex-direction: column; gap: 0.5rem; }
.form-row label {
  font-family: var(--font-label);
  font-size: 0.78rem;
  letter-spacing: 0.03em;
  color: var(--ink-dim);
  display: flex;
  align-items: center;
  gap: 0.6rem;
}
.required, .optional {
  font-size: 0.6rem;
  padding: 0.1em 0.55em;
  border-radius: 999px;
  letter-spacing: 0.02em;
}
.required { color: var(--wood-deep); background: var(--honey-500); }
.optional { color: var(--ink-faint); border: 1px solid var(--border); }

.form-row input, .form-row textarea {
  font-family: var(--font-jp-body);
  font-size: 0.95rem;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 0.8em 1em;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.form-row input:focus, .form-row textarea:focus {
  outline: none;
  border-color: var(--honey-500);
  box-shadow: 0 0 0 3px rgba(232, 169, 59, 0.18);
}
.form-row textarea { resize: vertical; }

.form-error { min-height: 1.1em; font-size: 0.76rem; color: #b3462f; }


.form-status {
  text-align: center;
  font-family: var(--font-label);
  font-size: 0.84rem;
  color: var(--honey-text);
  min-height: 1.2em;
}

/* =========================================================
   CONTACT — STRUCTURAL / FORM LAYER (was "SYSTEM ACCESS v3.3")
   The v3.3 black+green SYSTEM world this block used to open with is
   RETIRED: its controller and DOM went in U1-B (`f262a15`), and every
   rule that only existed for it — sysbg, DATA RAIN, ambient signal,
   corruption/blackframe/scanline entry storyboard, systag/sysmeta,
   terminal, channel label, sys-block readout — went in U1-C along
   with its four Desktop-only assets.

   What is left here is the CONTACT structure and form styling that
   the retired world happened to share: .contact--system (still a live
   class hook on the section, see index.html), .contact__topbar /
   __stable / __human-line / __grid, and the whole .contact-form--system
   layer. Nearly all of it is re-declared by "CONTACT — UNIFIED
   PRESENTATION (PHASE U1-A)" further down, which is what the visitor
   actually sees; these rules still carry the declarations U1-A does
   not restate (e.g. .contact--system's own overflow:hidden), so they
   are load-bearing, not dead. Folding the two layers into one is a
   later, cascade-level job — U1-C deliberately deleted only what
   provably matched nothing.
   ========================================================= */
:root {
  /* Retained: both are still consumed by the shadowed form/section layer
     below (8 and 4 consumers respectively), which U1-A does not fully
     restate. The name is legacy; the values are live.
     --sys-green-dark / --sys-green-mid / --system-honey-mask went in
     U1-C, and --sys-black in U1-D, each once its last consumer was
     deleted. */
  --sys-green-bright: #6dfa9a;
  --sys-green-label: #4fae7c;
}

/* U1-D: position / background / color / padding were removed here — the
   U1-A rule further down restates all four, so they were provably
   shadowed (measured: 0 rendered diff at 390 and 1440). `overflow` is
   the one declaration U1-A does NOT restate, so it stays and is now the
   entire reason this rule exists. */
.contact--system { overflow: hidden; }
/* ---------- Topbar ---------- */
.contact__topbar {
  position: relative;
  z-index: 7;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1.5rem;
}
.contact--system .section__head { margin-bottom: 0; }
.contact--system .section__title { color: #f3ecd9; }
.contact--system .section__title-jp { display: none; }
/* ---------- Stable state ---------- */
.contact__stable {
  position: relative;
  z-index: 7;
  margin-top: 1.9rem;
  opacity: 0;
  transition: opacity 0.5s ease;
}
.contact__message {
  /* Left, on the CONTACT column's own editorial axis -- not centred just
     because it is an image: the section head is --left and the note below it
     is text-align:left, so the artwork starts on the same rule.
     Width is capped at the form column (620px), not the 1180px panel:
     clearly larger than a speech-bubble UI, clearly not a hero, and it reads
     as the bridge between お問い合わせ and the paragraph.
     The asset's own transparent surround (~2.5% each side, ~11% top and
     bottom of its 2048x768 canvas) supplies part of the breathing room, so
     the margins here are deliberately modest -- no whitespace was added to
     stand in for the removed heading. No card, no panel, no shadow: the
     artwork carries its own bubble boundary and its transparent outside
     shows the CONTACT background. */
  width: min(100%, 620px);
  margin: 0.6rem 0 0;
  font-size: 0; /* the <h3> holds no text of its own; remove its line box */
}
.contact__message-img {
  display: block;
  width: 100%;
  height: auto; /* the width/height attributes hold the 2048x768 ratio -- CLS 0 */
  transition:
    opacity 0.9s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 0.9s cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* ONE reveal for the whole artwork -- portrait and bubble are a single asset
   and are never animated apart. The element carries the site's own `reveal`
   class, so the EXISTING observer fires it once on entry and then unobserves
   (no replay, no new JS). The site-wide .reveal opacity:0 is neutralised here
   and the pre-reveal state re-stated on the IMG behind
   html.has-reveal-observer, which js/main.js sets only once that observer is
   watching -- so a blocked or broken script leaves the artwork visible. */
.contact__message.reveal,
.contact__message.reveal.is-visible {
  opacity: 1;
  transform: none;
  transition: none;
}
html.has-reveal-observer .contact__message.reveal:not(.is-visible) .contact__message-img {
  opacity: 0;
  transform: translate3d(0, 10px, 0);
}
/* AMBIENT FLOAT — the artwork reads as very slightly suspended, never as an
   animated banner. It lives on its OWN layer: the entrance transition stays
   on the .contact__message-img and the infinite drift is on this wrapper, so
   the two transforms compose instead of overwriting each other and the
   revealed resting position is still exactly the approved one.
   It starts only once the existing observer has added .is-visible, and then
   only after a 1s delay -- the 900ms reveal has finished by then, so the
   entrance is never a compound movement. Because the trigger is .is-visible,
   a page without JS simply shows a static artwork, which is the preferred
   no-JS behaviour and keeps the progressive-enhancement gate intact.
   Vertical only, 3px at the top of the drift and 1px at the bottom, with the
   turns at 32% and 68% rather than 25/75 so the loop never reads as a
   mechanical up/down. Nothing else animates: no scale, rotation, skew,
   horizontal drift, shadow, filter or opacity. */
.contact__message-float {
  display: block;
}
.contact__message.reveal.is-visible .contact__message-float {
  animation: contactMessageFloat 6s ease-in-out 1s infinite;
}
@keyframes contactMessageFloat {
  0% { transform: translate3d(0, 0, 0); }
  32% { transform: translate3d(0, -3px, 0); }
  68% { transform: translate3d(0, 1px, 0); }
  100% { transform: translate3d(0, 0, 0); }
}
/* Reduced motion: present immediately -- no rise, no fade. This block has to
   sit HERE, directly after the rule it cancels: the site's other
   reduced-motion block lives up in the SNS section, i.e. EARLIER in the file,
   and at identical specificity the later rule wins -- measured, the artwork
   stayed at opacity 0 under prefers-reduced-motion until this moved down. */
@media (prefers-reduced-motion: reduce) {
  html.has-reveal-observer .contact__message.reveal:not(.is-visible) .contact__message-img,
  .contact__message-img { opacity: 1; transform: none; transition: none; }
  /* no entrance motion AND no ambient drift */
  .contact__message.reveal.is-visible .contact__message-float,
  .contact__message-float { animation: none; transform: none; }
}
.contact--system .contact__note {
  text-align: left;
  max-width: 34em;
  margin: 1.3rem 0 0;
  color: #b9c4bb;
}


/* ---------- Form column (dark theme override) ---------- */
.contact-form--system {
  max-width: none;
  padding: 0;
  gap: 0;
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
}
.contact-form--system .form-row { gap: 0.4rem; margin-bottom: 1.9rem; }
.contact-form--system .form-row--tight { margin-bottom: 1.55rem; }
.form-row__num {
  margin: 0 0 0.2rem;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  color: var(--sys-green-label);
}
.contact-form--system label {
  font-family: var(--font-jp-body);
  font-size: 1rem;
  color: #f3ecd9;
  display: flex;
  align-items: center;
  gap: 0.6rem;
}
.contact-form--system .required,
.contact-form--system .optional {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.05em;
  padding: 0;
  border-radius: 0;
  background: none;
  border: none;
}
.contact-form--system .required { color: var(--sys-green-label); }
.contact-form--system .optional { color: var(--ink-faint); }
.contact-form--system input,
.contact-form--system textarea {
  background: transparent;
  border: none;
  border-bottom: 1px solid rgba(239, 230, 210, 0.16);
  border-radius: 0;
  color: #f3ecd9;
  padding: 0.6em 0.1em;
}
.contact-form--system input:focus,
.contact-form--system textarea:focus {
  outline: none;
  border-bottom-color: var(--sys-green-bright);
  box-shadow: none;
}
.contact-form--system textarea { min-height: 190px; resize: vertical; }
.contact--system .form-error { color: #e0654a; }
.contact--system .form-status.is-error { color: #e0654a; }

/* REQUEST TYPE — restrained radio "channel selector", never a native
   select; simplified corner/bracket framing (thin top/bottom rule
   only, no heavy panel) */
.form-row--type { border: none; margin: 2rem 0; padding: 0; }
.form-row--type legend { display: block; width: 100%; padding: 0; }
.form-row--type .form-row__question {
  text-align: center;
  font-family: var(--font-jp-display);
  font-size: 1.05rem;
  color: #f3ecd9;
  margin: 0.6rem 0 1.4rem;
}
.request-type {
  border-top: 1px solid rgba(239, 230, 210, 0.1);
  border-bottom: 1px solid rgba(239, 230, 210, 0.1);
}
.request-type__row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.8rem;
  min-height: 46px;
  padding: 0.85em 0.4em;
  border-bottom: 1px solid rgba(239, 230, 210, 0.08);
  cursor: pointer;
}
.request-type__row:last-child { border-bottom: none; }
.request-type__row input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}
.request-type__dot {
  flex-shrink: 0;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 1px solid var(--sys-green-label);
  transition: background 0.2s ease, box-shadow 0.2s ease;
}
.request-type__text {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 0.05em;
  color: #c7d0c9;
  transition: color 0.2s ease;
}
.request-type__selected {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  color: var(--sys-green-bright);
  opacity: 0;
  transition: opacity 0.2s ease;
}
.request-type__row input:checked ~ .request-type__dot {
  background: var(--sys-green-bright);
  box-shadow: 0 0 6px var(--sys-green-bright);
}
.request-type__row input:checked ~ .request-type__text { color: #f3ecd9; font-weight: 700; }
.request-type__row input:checked ~ .request-type__selected { opacity: 1; }
.request-type__row input:focus-visible ~ .request-type__dot {
  outline: 2px solid var(--sys-green-bright);
  outline-offset: 2px;
}

/* Submit — restrained text+arrow action, no rounded button */
.contact-submit {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  margin-top: 1.6rem;
  padding-bottom: 0.5rem;
  font-family: var(--font-label);
  font-weight: 700;
  font-size: 1rem;
  color: #f3ecd9;
  border-bottom: 2px solid var(--sys-green-bright);
  transition: color 0.2s ease;
}
/* The hover / focus-visible colour that used to live here set the label to
   --sys-green-bright. It was a leftover from this block's original terminal
   palette: the honey theme below re-grounds the button (--ink on --honey-500)
   but never overrode the label colour, so green survived into both states and
   rendered #6dfa9a on #c67c1a -- 2.51, against the 4.5 this 16px/700 label
   needs. Removed at the root rather than overridden, so hover and focus now
   simply keep the button's own --ink from the honey rule (6.39). No existing
   token could have fixed it in place: on --honey-600 the darkest text token in
   the palette, --ink itself, reaches only 3.96. */
.contact-submit:focus-visible { outline: 2px solid var(--sys-green-bright); outline-offset: 4px; }
.contact--system .form-status { text-align: left; color: var(--sys-green-label); }

/* =========================================================
   HONEY DRIP — DESKTOP GEOMETRY + DIMMING
   SYSTEM HONEY (ASSET 04) is retired: the data-rendered green twin
   (__img--system), its data-motion sweep and its contamination layer
   were removed from index.html and from this stylesheet in U1-C,
   together with contact-system-honey-data.png. The master gold image
   (__img--normal, FROZEN) is the only honey layer that has ever been
   visible and is now the only one that exists.
   ========================================================= */
@media (min-width: 760px) {
  /* Locked to the FINAL master asset's own intrinsic ratio
     (honey-master-v2.webp, 1536x1024 = 1.5). It was 1672/941 for the
     previous honey-master.png; because __img--normal below is
     object-fit:contain inside this box, a stale ratio would letterbox the
     new master and pull it off the right edge, so this value is not free
     styling — it must equal the master's intrinsic ratio. */
  /* CANDIDATE 01 (comparison build): this value is not free styling — it must
     equal the master's own intrinsic ratio or object-fit:contain letterboxes it.
     FINAL honey-master-v2.webp = 1536 / 1024; candidate = 1312 / 1199. */
  .honey-drip { aspect-ratio: 1312 / 1199; }
  .honey-drip__img--normal {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
  }
}
/* v3.2 fix: rescoped to --img-normal only (was the generic `.honey-drip.is-dimmed img`,
   which also caught the retired SYSTEM twin). .is-dimmed is still set by
   initHoneyDim in js/main.js while CONTACT is on screen — this rule is live. */
.honey-drip.is-dimmed .honey-drip__img--normal {
  filter: saturate(0.87) brightness(0.92);
  transition: opacity 0.35s ease, filter 0.4s ease;
}


/* =========================================================
   GLOBAL HONEY — GOLD LOCK (PHASE U1-A, from a human live-QA find)
   History worth keeping: a user on a real PC reported the top-right
   honey turning green while scrolling toward CONTACT. Automated QA had
   not caught it. Two causes, both now gone at the source — the SYSTEM
   crossfade that swapped __img--normal for the green twin, and
   __data-motion's always-on green sweep (mask-image:none means
   *unmasked*, not hidden, so its 0.11-opacity gradient painted over
   the honey from page load).

   U1-A neutralised them with !important locks; U1-B removed the
   controller; U1-C removed the elements, their rules and their asset.
   The two locks that pinned the now-deleted layers to opacity 0 went
   with them. This one line stays: nothing in the stylesheet reduces
   __img--normal's opacity any more, so it is belt-and-braces rather
   than load-bearing — kept deliberately, because this exact surface
   regressed once in front of the owner and cannot be re-verified from
   the Claude environment (no live-site access). Remove it only with a
   real-device check in the same round. */
@media (min-width: 760px) {
  .honey-drip__img--normal { opacity: 1 !important; }
}

/* =========================================================
   GLOBAL HONEY v2 — ULTRA SIMPLE FALL DELTA (OPEN)
   Driven by js/main.js initHoneyV2(). PC LARGE DROP FIX: this
   block was scoped to the honey-drip mobile breakpoint
   (max-width:640px) with the layer display:none everywhere else,
   so on PC the large drop never existed and only the small
   .honey-drop moved (R1 audit item A-7). The layer now exists at
   every viewport; its box simply follows .honey-drip's own box at
   each breakpoint (desktop clamp() width / mobile 37vw crop) so
   the JS origin percentages land on the same drip tip. The
   FROZEN .honey-drip above is untouched — this is a separate,
   height:0, pointer-events:none layer behind it.

   Every previous round's staging (GROW/STRETCH/PRE-DETACH/DETACH
   animation/HOLD/BODY RECOIL/FALL HEAVY/mid-fall asset or color
   change) is gone. A DROP EVENT is now exactly one thing: DROP
   APPEAR -> SLOW FALL -> VIEWPORT OUT -> REMOVE.

   .honey-v2-drop (and .honey-v2-drop-small, the occasional
   smaller secondary drop) sit BEHIND the existing master honey
   (.honey-drip__img--normal, FROZEN, still the only visual —
   never replaced): z-index 44 vs .honey-drip's 45. There is no
   appear animation — js/main.js places the element already at
   its fixed origin point, which the master's own opaque drip tip
   naturally covers the top of, then immediately starts a single
   translateY transition, computed in real px (never vh/dvh) each
   event from the actual window.innerHeight at that moment. As it
   moves down and clears the master's silhouette the rest becomes
   visible on its own — no separate reveal step needed. Both carry
   a soft saturate/contrast filter so the more illustrated new
   material doesn't read as a different texture from the master
   image.
   ========================================================= */
.honey-v2-drop-layer {
  display: block;
  position: fixed;
  top: -2px;
  right: -2px;
  width: clamp(300px, var(--honey-coverage), 620px); /* = .honey-drip's desktop box */
  height: 0;
  pointer-events: none;
  z-index: 44; /* behind .honey-drip (45) so the origin point tucks under its own drip tip */
  overflow: visible;
}
@media (max-width: 640px) {
  .honey-v2-drop-layer {
    width: 37vw; /* = .honey-drip's mobile crop box */
    max-width: 210px;
  }
}

/* GLOBAL HONEY — CANDIDATE 02 / PASS B (comparison build, NOT adopted, NOT FROZEN).
   Candidate 02's mass sits lower in its own canvas than the FINAL master's, so on
   wide desktops the lowest drip nicked the HERO eyebrow's tail (631px^2 at 1440,
   name 0px). The measured correction is a -22px lift of the whole box:
   top -2px -> -24px.

   Scoped to min-width:1280px, not to the whole desktop range. Sweeping viewport
   width at Pass A geometry puts the first contact at ~1312px and none at all from
   1264px down, so below this breakpoint the lift removes no overlap and only costs
   visible honey (measured -29% of visible ink at 1024, -33% at 768). Everything
   under 1280px therefore keeps Pass A's approved top:-2px -- including the <=640px
   mobile crop window (37vw x 20vw), where a 22px lift would eat about a third of
   the visible box.

   .honey-v2-drop-layer must carry the SAME breakpoint and the same value. The drop
   origin is `layer top + originTop()`, and originTop() is derived from .honey-drip's
   HEIGHT (untouched here), so moving the layer by the identical -22px preserves the
   body-to-drop relationship exactly at every width -- verified as byte-identical to
   Pass A -- with no change to originTop(), TIP_ANCHORS, timing, sizes or the drop
   assets. If the two selectors ever drift apart, the honey moves and the falling
   drop does not. This block sits after both elements' base and mobile rules so it
   actually wins the cascade.

   To go back to Pass A everywhere: delete this block. To go back to the FINAL
   master: restore the img src in index.html and the aspect-ratio above. */
@media (min-width: 1280px) {
  .honey-drip { top: -24px; }
  .honey-v2-drop-layer { top: -24px; }
}
.honey-v2-drop,
.honey-v2-drop-small {
  display: none; /* only shown for the duration of one DROP EVENT — see below */
  position: absolute;
  left: var(--drop-x, 84%);
  top: var(--drop-y, 34px);
  transform: translate(-50%, 0);
  transition: none; /* js/main.js sets one inline transition + transform per event */
}
.honey-v2-drop.is-active,
.honey-v2-drop-small.is-active {
  display: block;
}
.honey-v2-drop { width: 34px; }
.honey-v2-drop-small { width: 15px; }
.honey-v2-drop img,
.honey-v2-drop-small img {
  display: block;
  width: 100%;
  height: auto;
  filter: saturate(0.85) contrast(0.92) brightness(1.02);
}

@media (prefers-reduced-motion: reduce) {
  .honey-v2-drop-layer { display: none; }
}

/* =========================================================
   CONTACT — UNIFIED PRESENTATION (PHASE U1-A)
   The ORGANIC v1.1 rules below (originally Mobile-only, <=759px) are
   now the section's base presentation at every width. They still win
   over CONTACT — SYSTEM ACCESS above via the normal cascade (equal
   selector specificity, later in source), so nothing above this block
   needed to change. The SYSTEM DOM (js/main.js initContactSystem/
   initSystemHoney: the entry storyboard, DATA RAIN, SYSTEM HONEY,
   HONEY ERROR) is still present and still runs — the hide list right
   below keeps it invisible at every width instead of only <=759px.
   Removing that controller/DOM/its 4 Desktop-only assets is PHASE
   U1-B/U1-C, not this commit — U1-A is presentation only.

   The @media (min-width:760px) block at the end of this section is
   new: it adapts the same single-column shell to wider viewports
   (container max-width, fluid spacing/type, CTA width) instead of
   stretching the 390px layout edge-to-edge. It intentionally does not
   touch assets/section-bg/section-bg-07-forest-world.webp (the
   FROZEN §0.35 SECTION BACKGROUND PROTOTYPE 01 block above, shared
   with HERO/PROFILE/NEWS/WORKS/SNS) — the forest-edge traces already
   inside this block (assets/graphics/forest-layer-a.webp, the same
   file PROFILE already loads at every width) are enough to answer
   Desktop whitespace without touching that shared block.
   ========================================================= */

/* back to the site's own cream/ivory/honey/wood world */
.contact--system {
  position: relative;
  background: var(--bg);
  color: var(--ink);
  padding: 4.5rem 20px 6.5rem;
}
.contact--system .section__title { color: var(--ink); }
.contact--system .section__title-jp { display: block; }
.contact__topbar { display: block; }

/* v1.1: a very faint trace of the forest-edge texture other sections
   already use (assets/graphics/forest-layer-a.webp, reused — no new
   asset), so CONTACT reads a little less like a bare color swap and
   a little more like paper set down at the edge of the woods. Sits
   well under the topbar/form content (z-index 0 vs their 7). */
.contact--system::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 140px;
  max-height: 20%;
  background-image: url("../assets/graphics/forest-layer-a.webp");
  background-repeat: no-repeat;
  background-position: center bottom;
  background-size: min(640px, 130%) auto;
  opacity: 0.13;
  z-index: 0;
  pointer-events: none;
}
/* FINAL POLISH 01: the same reused texture, mirrored, as a much
   fainter hint at the top-right edge only — not a duplicate of the
   bottom trace, just enough that the organic world feels like it
   continues past the header instead of the section reading flat
   until the forest reappears at the very bottom. Sits behind the
   topbar (z-index 0 vs 7) and stays clear of the headline/FORM
   column entirely. */
.contact--system::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  width: 58%;
  height: 170px;
  max-height: 24%;
  background-image: url("../assets/graphics/forest-layer-a.webp");
  background-repeat: no-repeat;
  background-position: right top;
  background-size: min(460px, 150%) auto;
  transform: scaleY(-1);
  opacity: 0.07;
  z-index: 0;
  pointer-events: none;
}

/* no JS-driven reveal on mobile any more — always visible, exactly
   like every other section's content (the section itself still gets
   the shared .reveal scroll-fade-up via its own "reveal" class).
   v1.1: with EXTERNAL CHANNEL / 06 removed above, the headline is
   now .contact__stable's first line — a bit more margin-top gives
   the "06/06 GET IN TOUCH / CONTACT / お問い合わせ" block room to
   breathe before it. */
.contact__stable { opacity: 1; margin-top: 2.6rem; }

.contact--system .contact__note {
  color: var(--ink-dim);
  font-size: 0.9rem;
  line-height: 1.9;
  margin-top: 1rem;
  text-align: left;
  max-width: none;
}

.contact__grid { margin-top: 2.2rem; display: block; }

/* ---------- FORM — same skeleton kept (number / technical label /
   Japanese label / thin rule), translated to the organic palette
   instead of ivory-on-black. No rounded card, no cream input box —
   same restrained language as the SYSTEM version, just re-colored. ---------- */
.contact-form--system .form-row { margin-bottom: 1.7rem; }
.contact-form--system .form-row--tight { margin-bottom: 1.4rem; }
/* D-2: --ink-dim, not a honey token. At Mobile these numbers sit over
   .contact__section-bg -- the FOREST WORLD photo at opacity 0.55 -- so the
   rendered ground is nothing like a flat --bg: --honey-text measured 3.71 at
   the worst pixel and even --honey-text-deep only 4.39. --ink-dim is the
   smallest existing-token step that clears AA everywhere (5.35 at 390, 6.00 at
   1440). CONTACT keeps its honey through REQUIRED/OPTIONAL, the form status and
   the submit button, so only the step numbering changes register. */
.form-row__num { color: var(--ink-dim); margin-bottom: 0.4rem; }
.contact-form--system label { color: var(--ink); font-size: 0.95rem; }
.contact-form--system .required { color: var(--honey-text-deep); }
/* OPTIONAL shares REQUIRED's colour token. The two markers are otherwise
   identical already -- same mono face, 9.92px, weight, letter-spacing,
   line-height, zero padding, no border, transparent ground -- so colour was
   the only thing setting them apart, and --ink-faint-text read as a
   different kind of label rather than the same status marker. Reusing
   --honey-text-deep (not a new value) also keeps the tuned contrast pair:
   both tokens are the 4.51-on-bg-soft / 4.92-on-bg accessible pair, so this
   is a consistency fix with no contrast cost. */
.contact-form--system .optional { color: var(--honey-text-deep); }
.contact-form--system input,
.contact-form--system textarea {
  color: var(--ink);
  border-bottom-color: var(--border);
}
.contact-form--system input:focus,
.contact-form--system textarea:focus {
  border-bottom-color: var(--honey-500);
}
/* KEYBOARD FOCUS on the text controls (PHASE D / D-1, WCAG 2.2 SC 1.4.11).
   The shipped system theme sets `outline: none; box-shadow: none` on :focus
   above, which left the bottom border's colour change as the whole indicator:
   --honey-500 against --bg measures 1.87, under the 3:1 a focus indicator
   needs, and the unfocused->focused step is only 1.29. A keyboard user could
   not reliably tell which field they were in.

   This restores an indicator without touching the border, so the existing
   focus, validation and confirm-state styling all still read exactly as
   before -- the outline is drawn outside the border box and is purely
   additive. Nothing here changes geometry: `outline` never affects layout,
   and no dimension, padding or radius is touched.

   The treatment is the site's own focus idiom, not a new one: 2px solid +
   2px offset, the same as .menu-trigger:focus-visible and the REQUEST
   TYPE dot. The colour is --honey-text-deep, already used two rules above
   for .required / .optional and documented as the accessible pair --
   4.92 on --bg, 4.51 on --bg-soft, so it clears 3:1 on both grounds with
   room to spare. No new token, no palette change.

   :focus-visible, not :focus, so a mouse click still leaves no ring.
   Radios are excluded: their input is a 1x1 opacity:0 proxy target, and the
   visible .request-type__dot carries its own outline (below). */
.contact-form--system input:not([type="radio"]):focus-visible,
.contact-form--system textarea:focus-visible {
  outline: 2px solid var(--honey-text-deep);
  outline-offset: 2px;
}
.contact--system .form-error { color: #b3462f; }
.contact--system .form-status.is-error { color: #b3462f; }

.form-row--type { margin: 1.6rem 0; }
.form-row--type .form-row__question { color: var(--ink); font-size: 0.95rem; text-align: left; }
.request-type { border-color: var(--border); }
.request-type__row { border-bottom-color: var(--border); }
.request-type__dot { border-color: var(--wood); }
.request-type__text { color: var(--ink-dim); }
/* REQUEST TYPE selected state: honey gold, not SYSTEM green — small
   dot + label weight only, no pill/card */
.request-type__row input:checked ~ .request-type__dot {
  background: var(--honey-500);
  box-shadow: 0 0 4px rgba(232, 169, 59, 0.5);
}
.request-type__row input:checked ~ .request-type__text { color: var(--ink); }
/* v1.1: "CHANNEL SELECTED" removed — the gold dot + the label's own
   weight/color shift above are enough feedback on their own; radio
   semantics (aria/focus/keyboard) are untouched. */
.request-type__selected { display: none; }
/* Same D-1 correction, on the REQUEST TYPE radios. The indicator already
   existed here -- the radio input is a 1x1 opacity:0 proxy target and the
   visible .request-type__dot carries the 2px/2px outline from :4822 -- but it
   was drawn in --honey-500, the same 1.87-on---bg colour as the text fields'
   border. Only the colour moves, to the same --honey-text-deep used above;
   the outline width, offset, the dot and every other radio state are
   untouched. */
.request-type__row input:focus-visible ~ .request-type__dot { outline-color: var(--honey-text-deep); }

/* CTA — FINAL CONTRAST DELTA: filled honey-gold pill (was cream fill
   with a gold outline; the cream read too faint against the FOREST
   background). Same family as the restored PROFILE role-tag pills
   (honey fill + wood-deep text + capsule radius), one step stronger
   since this is the action button, not a label — size/padding/radius
   unchanged. Arrow glyph hidden here only (Desktop's own CTA/arrow is
   untouched).
   SUBTLE PRESS MOTION: a quiet "breathe" loop (idle almost all of the
   cycle, one ~1.5px dip and back) hints it's pressable without ever
   drawing attention on its own; a real tap dips it slightly deeper.
   Paused entirely under prefers-reduced-motion. */
.contact-submit {
  color: var(--ink); /* MOBILE VISUAL SYSTEM FINAL CHECK: darker than
    wood-deep for instant read against the fill; stays in the site's
    brown/wood family (--ink is the same dark-brown ink used for body
    copy elsewhere), not black */
  background: var(--honey-500);
  border: 1.5px solid var(--honey-600);
  border-radius: 999px;
  padding: 0.7rem 1.6rem;
  min-height: 44px;
  align-items: center;
  justify-content: center;
  animation: contact-cta-breathe 3.4s ease-in-out infinite;
}
.contact-submit span { display: none; }
/* The background no longer darkens to --honey-600 on hover / focus. That step
   was what put the label on a mid-tone ground where nothing in the palette
   could reach 4.5 -- even --ink only managed 3.96 there. Keeping --honey-500
   from the rest state instead restores the button's own approved pairing,
   --ink on --honey-500 at 6.39, for every state.
   The state cue is not lost: border-color still steps --honey-600 ->
   --honey-text, and keyboard focus additionally draws the --honey-text-deep
   ring below (4.92 on --bg). */
.contact-submit:hover, .contact-submit:focus-visible { border-color: var(--honey-text); }
/* Same D-1 correction again, on the submit button. Its 2px/4px outline from
   :4842 was already present, but in --honey-500: with a 4px offset the ring
   floats clear of the button and sits on --bg, where it measures 1.87, and
   1.61 against the button's own --honey-600 ground on the inner edge. Only
   the colour moves, to the --honey-text-deep used by the fields and the
   REQUEST TYPE dot; width, style, offset, the button's geometry, colours,
   hover, active and submitting states are all untouched. */
.contact-submit:focus-visible { outline-color: var(--honey-text-deep); }
.contact-submit:active {
  animation: none;
  transform: translateY(2.5px) scale(0.985);
  transition: transform 0.08s ease-out;
}
@keyframes contact-cta-breathe {
  0%, 84% { transform: translateY(0) scale(1); }
  90% { transform: translateY(1.5px) scale(0.99); }
  96%, 100% { transform: translateY(0) scale(1); }
}
.contact--system .form-status { color: var(--honey-text-deep); text-align: left; }

/* one small botanical trace at the section's bottom edge, reusing
   the exact inline-SVG leaf-decor pattern PROFILE/WORKS/SNS already
   use (no new asset) — kept out of the form's reading column */
.leaf-decor--contact { display: block; }
@media (prefers-reduced-motion: reduce) {
  .contact-submit { animation: none; }
}

/* ---------- CONFIRM STEP (STEP 2A) ----------
   An inline state of the same form, in the same organic world: existing
   tokens only, no new colour, no new asset, no animation of any kind, and
   deliberately NO position/z-index — .contact__topbar / .contact__stable
   carry z-index:7 to stay above the forest ::after band, and adding another
   stacking context here is exactly the kind of paint-order change geometry
   comparison cannot detect. The panel inherits the section's 680px column at
   >=760px through .contact__stable, so it needs no breakpoint of its own. */

/* MANDATORY, not tidy-up: the display below beats the UA sheet's
   [hidden] { display: none }, so without this rule the panel would be visible
   from first paint. Same guard as .menu-overlay[hidden] / .news-more[hidden] /
   .career-archive[hidden] above. */
.contact-confirm[hidden] { display: none; }
.contact-confirm {
  display: block;
  margin-top: 0.6rem;
}
.contact-confirm__title {
  margin: 0 0 1.2rem;
  font-family: var(--font-jp-display);
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--ink);
}
/* the heading takes focus on entering the step (js/main.js), which is what
   tells a screen reader the step changed — no aria-live is added anywhere */
.contact-confirm__title:focus-visible {
  outline: 2px solid var(--honey-text-deep);
  outline-offset: 2px;
}
.contact-confirm__row {
  padding: 0.9rem 0;
  border-bottom: 1px solid var(--border);
}
/* the optional 会社名 row is hidden with the hidden attribute when blank; the
   row has no display of its own, but state it anyway so a later layout change
   cannot silently re-break it */
.contact-confirm__row[hidden] { display: none; }
.contact-confirm__label {
  margin: 0 0 0.35rem;
  font-family: var(--font-label);
  font-size: 0.78rem;
  letter-spacing: 0.03em;
  color: var(--ink-dim);   /* never --ink-faint: it does not reach AA */
}
.contact-confirm__value {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.9;
  color: var(--ink);
  /* a long address or an unbroken string must wrap rather than widen the page
     at 320px — the section is overflow:hidden, so an overflow here would clip
     rather than scroll */
  overflow-wrap: anywhere;
}
/* the message keeps the visitor's own line breaks and simply makes the page
   taller — no inner scroll container, which on mobile would trap the scroll
   and hide the notice and the CTA below it */
.contact-confirm__value--message { white-space: pre-wrap; }
.contact-confirm__notice {
  margin: 1.4rem 0 0;
  font-size: 0.84rem;
  line-height: 1.9;
  color: var(--ink-dim);
}
/* the quieter of the two actions: the site's capsule shape and honey ink, but
   outlined rather than filled, so the gold CTA below stays the primary action */
.contact-confirm__edit {
  margin-top: 1.4rem;
  padding: 0.6rem 1.5rem;
  min-height: 44px;
  font-family: var(--font-label);
  font-size: 0.86rem;
  font-weight: 700;
  color: var(--honey-text-deep);
  border: 1.5px solid var(--border-strong);
  border-radius: 999px;
}
.contact-confirm__edit:hover { border-color: var(--honey-500); color: var(--honey-text); }
.contact-confirm__edit:focus-visible {
  outline: 2px solid var(--honey-text-deep);
  outline-offset: 2px;
}

/* Input rows only. Deliberately NOT a "hide everything in the form" selector:
   the honeypot input, .contact-submit and .form-status all sit outside
   .form-row and must stay exactly as they are. display:none (not opacity or
   visibility) is required — a hidden-but-focusable field would be a keyboard
   trap. .form-row is display:flex at (0,1,0), so this (0,2,0) rule is what
   wins; the hidden attribute could not do this job here. */
.contact-form--confirming .form-row { display: none; }

/* =========================================================
   CONTACT — DESKTOP/TABLET ADAPTATION (PHASE U1-A)
   Same single-column ORGANIC shell above, capped and centered instead
   of filling the viewport. 680px matches PHASE U0's validated
   prototype (10 widths, 0 overflow). No 2-column grid, no SYSTEM
   sidebar revival — layout/spacing/type scale only, per DELTA scope.
   ========================================================= */
@media (min-width: 760px) {
  .contact--system {
    /* PATCH 001: top edge follows the shared 5rem section rhythm (was
       clamp(4.5rem, …, 7rem) = ~95px at 1440); bottom = footer seam, unchanged. */
    padding: 5rem 1.5rem clamp(6.5rem, 4.862rem + 3.448vw, 9rem);
  }
  .contact__topbar,
  .contact__stable {
    max-width: 680px;
    margin-left: auto;
    margin-right: auto;
  }
  .contact__stable { margin-top: clamp(2.6rem, 2.207rem + 0.828vw, 3.2rem); }
  .contact--system .contact__note { max-width: 38em; }
  /* the gold-pill CTA stretches to the (narrow) form width on Mobile via
     .contact-form's flex stretch default — at 680px that reads too wide
     for a button, so it takes its own content width here instead */
  .contact-submit {
    width: auto;
    min-width: 280px;
    align-self: flex-start;
  }
  /* forest-edge traces from the block above: sized to keep reading as
     the same crop, not a stretched blow-up, as the section widens */
  .contact--system::before {
    height: clamp(140px, 60px + 8vw, 220px);
    background-size: min(1100px, 100%) auto;
  }
  .contact--system::after {
    width: 44%;
    height: clamp(170px, 100px + 8vw, 240px);
    background-size: min(720px, 140%) auto;
  }
}

/* =========================================================
   FOOTER / TO-TOP
   ========================================================= */
/* FOOTER COMPACTION (v1.0 final visual DELTA): a short closing band, not a
   section of its own. The centred hexagon ornament and its reserved space
   are gone (DOM and CSS), the top padding is a short pause after CONTACT's
   forest edge instead of a section-sized gap, and the bottom padding is
   only what keeps the copyright clear of the fixed TOP control in the
   bottom-right corner (48px button + 20px offset on Mobile, 46px + 1.3rem
   on Desktop, both lifted by the iOS home-indicator inset, hence the
   env() term; 5.5rem leaves the text ~18px above the floating TOP label
   at rest and ~12px at the top of its 6px float). The ground is a light
   honey tint mixed from two existing tokens rather than a new colour;
   --bg-soft is the plain-colour fallback for engines without color-mix().
   Copyright text, size and colour are untouched. */
.site-footer {
  position: relative;
  text-align: center;
  padding: 1.6rem 1.5rem 5.5rem;
  padding-bottom: calc(5.5rem + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--border);
  background: var(--bg-soft);
  background: color-mix(in srgb, var(--honey-300) 22%, var(--bg));
  color: var(--ink-dim);
  font-size: 0.78rem;
}
.site-footer__test { margin-top: 0.5rem; opacity: 0.75; }
.tbd-inline { margin-left: 0.5em; }

.to-top {
  position: fixed;
  right: 1.3rem;
  bottom: 1.3rem;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(145deg, var(--honey-500), var(--honey-600));
  color: var(--wood-deep);
  font-size: 1rem;
  box-shadow: 0 8px 18px rgba(58, 46, 32, 0.2);
  opacity: 0;
  transform: translateY(12px);
  pointer-events: none;
  transition: opacity 0.3s ease, transform 0.3s ease;
  z-index: 120;
}
.to-top.is-visible { opacity: 1; transform: translateY(0); pointer-events: auto; }

/* BACK TO TOP UI — HACHIKIRIN, Mobile 390 only. Desktop keeps the
   orange circle+arrow above untouched (this stays display:none there).
   The character image itself IS the UI — no circle/honey background,
   no frame, no speech bubble, no label added around it. */
.to-top-mobile {
  display: none;
  z-index: 120;
}
@media (max-width: 759px) {
  .to-top {
    /* Mobile swaps to the hachikirin button below; Desktop's own
       rule/behavior above is untouched. */
    display: none;
  }
  .to-top-mobile {
    display: block;
    position: fixed;
    right: 16px;
    bottom: 20px;
    /* safe-area fallback: a plain px value first, then progressively
       enhanced — engines that don't parse env() at all discard the
       second declaration and keep the first, rather than losing the
       position entirely. */
    right: calc(16px + env(safe-area-inset-right, 0px));
    bottom: calc(20px + env(safe-area-inset-bottom, 0px));
    width: 48px;
    height: 48px;
    padding: 0;
    border: none;
    background: transparent;
    box-shadow: none;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.3s ease, transform 0.12s ease, bottom 0.25s ease;
  }
  .to-top-mobile.is-visible { opacity: 1; pointer-events: auto; }
  /* TAP FEEDBACK only — the idle float animation below lives on the
     inner image instead, so the two transforms never fight. */
  .to-top-mobile:active { transform: scale(0.95); }
  /* CONTACT COLLISION FIX — only while the submit button is near the
     viewport's bottom edge (toggled in js/main.js), lift hachikirin
     clear of it. Compound selector so this reliably wins over the
     bottom rules above regardless of source order. Everywhere else
     (including elsewhere within CONTACT) the normal bottom:20px holds. */
  .to-top-mobile.to-top-mobile--avoid {
    bottom: 84px;
    bottom: calc(84px + env(safe-area-inset-bottom, 0px));
  }
  /* TOP BACKGROUND LABEL (prototype, pending human visual QA).
     Sits behind the character's head rather than on it: the character stays
     the thing you read first and the word is what you notice second. Width
     is a share of the button box (70% of 48px = 33.6px) so it tracks the UI
     rather than being a fixed pixel size, and height comes from the asset's
     own 2.207 ratio. Deliberately overlapped by the head - the covered part
     of the word is what makes the two layers read as foreground and
     background instead of two flat stickers.
     It is absolutely positioned and NOT animated, so the button's own fixed
     position, :active scale and --avoid shift are all untouched, and every
     drift of the character changes the overlap a little. pointer-events:none
     keeps the <button> the only hit target. */
  /* VISUAL UNIT (V3). TOP and the character are animated together from this
     one wrapper, so the word is part of the icon rather than a backdrop it
     drifts across: both children take the identical transform and their
     relative positions never change. It fills the button box exactly
     (inset:0), so the label's own top/left keep the coordinates they had
     when they resolved against the button itself. The button outside stays
     transform-free and keeps position:fixed, the click and --avoid. */
  .to-top-mobile__visual {
    position: absolute;
    inset: 0;
    display: block;
    /* slightly below centre: pivoting at 50% 50% swings the TOP label, which
       sits above the box, further than the character and reads as the word
       leading the tilt. 55% sits nearer the middle of the combined unit, so
       the whole thing leans as one body in the air rather than pivoting
       around a point at its feet. */
    transform-origin: 50% 55%;
    animation: hachikirin-float 4.2s ease-in-out infinite alternate;
  }
  .to-top-mobile__label {
    position: absolute;
    /* -4px, chosen by rendering 0/-4/-6/-8 against the actual artwork: at 0
       the head's honey tuft cuts through the middle of the word and it stops
       being readable, at -6/-8 it clears the head entirely and starts reading
       as a separate badge floating above rather than something behind. -4
       keeps the word legible while its lower edge disappears behind the tuft
       and ears, which is the overlap that makes it sit BEHIND the head.
       Purely visual and outside the button's 48x48 box, so the hit area,
       fixed position and --avoid shift are all unaffected. */
    top: -4px;
    left: 50%;
    width: 70%;
    /* the asset's own ratio, since a background has no intrinsic size to
       derive height from the way an <img> would */
    aspect-ratio: 256 / 116;
    transform: translateX(-50%);
    background-image: url("../assets/graphics/hachikirin/top-label.webp");
    background-size: 100% 100%;
    background-repeat: no-repeat;
    z-index: 0;
    pointer-events: none;
  }
  .to-top-mobile__img {
    /* positioned only to sit above the label in paint order - no offsets, so
       the character's own box, size and place are exactly as before */
    position: relative;
    z-index: 1;
    width: 100%;
    height: 100%;
    display: block;
    object-fit: contain;
  }
  @media (prefers-reduced-motion: reduce) {
    .to-top-mobile__visual { animation: none; transform: none; }
  }
}
@keyframes hachikirin-float {
  /* V3.1 (final, human-QA approved). V3's direction was approved as-is --
     the visual unit, the vertical lift and the flat 2D lean all stay
     exactly as they were -- only the lean's angle grows from +-4deg to
     +-6deg for more of the intended cuteness. There is still no
     translateX at any point of the cycle and the unit never changes its
     position across the screen. The lean is still a flat, in-plane 2D
     rotation: strictly translateY() and rotate(), with no
     rotateX/rotateY/perspective/scale, so nothing turns to face away from the
     viewer and nothing reads as depth.
     The tilt still passes through 0deg at the top and bottom of the rise
     rather than at the mid-point of a straight -6 -> +6 sweep. That is what
     keeps it from ticking like a metronome: each quarter of the cycle is one
     unbroken move, lift-and-lean-left, settle upright, sink-and-lean-right,
     settle upright, and it cycles without ever stopping dead at an
     extreme. */
  0% { transform: translateY(0) rotate(0deg); }
  25% { transform: translateY(-3px) rotate(-6deg); }
  50% { transform: translateY(-6px) rotate(0deg); }
  75% { transform: translateY(-3px) rotate(6deg); }
  100% { transform: translateY(0) rotate(0deg); }
}

/* =========================================================
   REVEAL-ON-SCROLL — restrained: a section fades up once as it
   enters the viewport, nothing repeats, nothing parallaxes

   M1 MOTION FOUNDATION — SAFE GATE. The pre-hidden state used to sit on
   .reveal itself, so with the script blocked, failed, or run without
   IntersectionObserver the five sections stayed at opacity 0 for ever
   (M0 finding). The default is now the FINAL state; the pre-state exists
   only under html.has-reveal-observer, the class js/main.js adds after its
   observer is watching -- the same progressive-enhancement gate the SNS
   artworks and the CONTACT artwork already use. Distance, duration and
   easing of the fade-up itself are unchanged (18px / 0.7s ease).
   :where() keeps the gated selector at (0,1,1) so it can never outrank
   the (0,2,0) neutralisations on .sns-gallery__item.reveal and
   .contact__message.reveal, whose motion lives on an inner element.
   The resting transform stays translateY(0), not none: it is what makes
   each section a stacking context, which its z-index:-1 leaf decoration
   relies on. Entering the pre-state is instant (transition: none there),
   so only the reveal itself ever animates.
   ========================================================= */
.reveal {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.7s ease, transform 0.7s ease;
}
html.has-reveal-observer :where(.reveal:not(.is-visible)) {
  opacity: 0;
  transform: translateY(18px);
  transition: none;
}
.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { transition-duration: 0.01ms !important; }
  .reveal { opacity: 1; transform: none; }
  html.has-reveal-observer :where(.reveal:not(.is-visible)) { opacity: 1; transform: none; }
}

/* =========================================================
   MOTION FOUNDATION v1 (M1) — 「現れる」のではなく「ピントが合う」
   The shared layer the later phases build on (M2 NEWS rows, M3 PROFILE,
   M4 WORKS, M5 CONTACT). Nothing here moves on its own: an element opts
   in with data-motion="<kind>", js/main.js (initMotionFoundation) claims
   it with data-motion-state="wait", and one IntersectionObserver flips it
   to data-motion-state="in" exactly once.

   SAFETY BY CONSTRUCTION. The pre-hidden state needs BOTH
   html.has-motion-observer (added only after the observer exists and is
   watching) AND data-motion-state="wait" (set per element by the script).
   With the script blocked, failed, IntersectionObserver missing, or an
   element the script never claimed, the default below -- fully visible,
   no transform, no filter -- is what renders. Reduced motion resolves to
   that same final state at once, with no stagger to wait for.

   Vocabulary: opacity, translateY, blur, scale, stagger, on the site's
   existing ease-out curve. No infinite, scroll-linked or rAF-driven
   motion lives here, and no will-change. Blur is per kind and capped at
   4px on Mobile.
   ========================================================= */
:root {
  --motion-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --motion-micro: 380ms;
  --motion-normal: 600ms;
  --motion-premium: 900ms;
  --motion-stagger: 60ms;
}
/* default = FINAL state. Only the transition is declared, so an element the
   script has not claimed renders exactly as it does today. */
[data-motion] {
  --motion-dur: var(--motion-normal);
  --motion-from: 0;
  --motion-rise: 8px;
  --motion-scale: 1;
  --motion-filter: none;
  transition:
    opacity var(--motion-dur) var(--motion-ease),
    transform var(--motion-dur) var(--motion-ease),
    filter var(--motion-dur) var(--motion-ease);
  /* --motion-i is the element's index inside the batch that entered the
     viewport together (set inline by the observer, capped at 5), so a list
     staggers only when it arrives as a group; --motion-delay is a fixed
     lead a phase may give one element. */
  transition-delay: calc(var(--motion-i, 0) * var(--motion-stagger) + var(--motion-delay, 0ms));
}
/* KINDS -- the M0 vocabulary as initial presets. A later phase tunes its
   own numbers here, never the mechanism. */
[data-motion="heading"] { --motion-dur: var(--motion-premium); --motion-rise: 14px; --motion-filter: blur(6px); }
[data-motion="image"]   { --motion-dur: var(--motion-premium); --motion-from: 0.3; --motion-rise: 0px; --motion-scale: 0.985; --motion-filter: blur(4px); }
[data-motion="card"]    { --motion-dur: 700ms; --motion-from: 0.45; --motion-rise: 0px; --motion-scale: 0.985; --motion-filter: blur(2px); --motion-stagger: 120ms; }
[data-motion="row"]     { --motion-rise: 8px; }
[data-motion="block"]   { --motion-rise: 6px; }
@media (max-width: 759px) {
  [data-motion="heading"] { --motion-filter: blur(4px); }
}
/* PRE-STATE -- only behind the gate, only on a claimed element. */
html.has-motion-observer [data-motion][data-motion-state="wait"] {
  opacity: var(--motion-from);
  transform: translate3d(0, var(--motion-rise), 0) scale(var(--motion-scale));
  filter: var(--motion-filter);
  transition: none; /* entering the pre-state is instant; only the way out animates */
}
@media (prefers-reduced-motion: reduce) {
  html.has-motion-observer [data-motion][data-motion-state="wait"] { opacity: 1; transform: none; filter: none; }
  [data-motion] { transition: none; }
}

/* ---- M1 REPRESENTATIVE TARGET: the NEWS heading ----
   One low-risk, below-the-fold target to validate gate, observer,
   activation, easing, reduced motion and no-JS on a real device. The
   section-level fade-up (the .reveal block above) is retired for #news
   only, the way .sns-gallery__item.reveal and .contact__message.reveal
   already neutralise it, so the heading's focus reveal is not stacked on
   an 18px section rise; translateY(0) is kept (stacking context, see the
   .reveal note). The NEWS rows therefore sit still until M2 gives them
   their own stagger -- an intended, temporary intermediate state. To go
   back to the section fade: delete this rule and the data-motion
   attribute on the NEWS head. */
#news.reveal,
#news.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
  transition: none;
}
/* =========================================================
   M2 NEWS MOTION — 「現在の活動が静かに動き始める」 (★★★☆☆)
   ONE ENCOUNTER. The NEWS heading (the M1 target: claimed and observed
   once by initMotionFoundation) is the only trigger; the rows follow its
   state by CSS alone. Heading focuses at T0 (M1 treatment, untouched);
   row n settles at T0 + --news-lead + n * --motion-stagger with opacity
   + an 8px rise, no blur, --motion-normal on --motion-ease. n is the
   row's DOM position capped at 4, so the last delay stays ~240ms however
   long the list gets.
   WHY THE ROWS ARE NOT data-motion TARGETS OF THEIR OWN: each row would
   then wait for its own -12% crossing, so the lower rows would sit
   invisible until a later scroll position (not one encounter), and the
   heading→row offset would depend on scroll speed. Following the heading
   makes the offset deterministic and keeps the rows inside the M1 safe
   gate: the pre-state needs html.has-motion-observer AND a heading the
   script actually claimed. No JS, no IntersectionObserver, no claim ->
   rows render as they always did. A row rendered after the heading is
   already "in" (slow JSON, deep link) is simply final: no dead state, no
   replay. VIEW MORE rows are display:none until the button, and by then
   the heading is "in", so they land static.
   The existing .news-item transition (padding-left / color: the hover
   contract, see the NEWS block) is restated as longhands and extended,
   not replaced: entries 1-2 keep 0.2s ease with no delay, entries 3-4
   carry the entrance. The <a> inside a linked row keeps its own rule.
   ========================================================= */
#news { --news-lead: 150ms; } /* first row starts this long after the heading */
.news-list > .news-item { --news-i: 0; }
.news-list > .news-item:nth-child(2) { --news-i: 1; }
.news-list > .news-item:nth-child(3) { --news-i: 2; }
.news-list > .news-item:nth-child(4) { --news-i: 3; }
.news-list > .news-item:nth-child(n+5) { --news-i: 4; } /* cap: row 5 and every later row */
.news-item {
  --news-delay: calc(var(--news-lead, 0ms) + var(--news-i, 0) * var(--motion-stagger));
  transition-property: padding-left, color, opacity, transform;
  transition-duration: 0.2s, 0.2s, var(--motion-normal), var(--motion-normal);
  transition-timing-function: ease, ease, var(--motion-ease), var(--motion-ease);
  transition-delay: 0s, 0s, var(--news-delay), var(--news-delay);
}
/* PRE-STATE: only behind the M1 gate, only while the claimed heading waits. */
html.has-motion-observer #news > .section__head[data-motion-state="wait"] ~ .news-list > .news-item {
  opacity: 0;
  transform: translate3d(0, 8px, 0);
  transition: none; /* entering the pre-state is instant; only the way out animates */
}
@media (prefers-reduced-motion: reduce) {
  html.has-motion-observer #news > .section__head[data-motion-state="wait"] ~ .news-list > .news-item { opacity: 1; transform: none; }
  .news-item { transition-delay: 0s; }
}
/* =========================================================
   M3 PROFILE MOTION — "QUIET ENCOUNTER" 「人物との静かな対面」
   Three groups, one breath: the heading (M1 kind, entry-unlocked so the
   encounter waits for SITE ENTRY to lift when PROFILE is already in view
   at load), the portrait frame and the whole text column. Nothing inside
   the column is staggered on its own -- name, roles, statement and copy
   settle together. Same mechanism as M2: the heading is the only element
   the M1 observer watches; the portrait and the column follow its state
   by CSS, so the timing is fixed and the M1 safe gate covers all three
   (no JS / no IntersectionObserver / no claim -> everything renders as it
   always did; reduced motion -> final at once; never replayed).
   heading  T0        M1 heading treatment, untouched (900ms, 14px, blur 6/4)
   portrait T0+90ms   opacity + 18px (12px <=759) + scale .985, 950ms, no blur
   column   T0+220ms  opacity + 10px, 800ms, no blur   (T0+200ms <=759)
   Last element settles at ~1.17s. The PROFILE section fade (.reveal) is
   retired here the way #news is, so the three motions are not stacked on
   an 18px section rise; translateY(0) is kept (stacking context for the
   z-index:-1 leaf decor). The marquee and QUALIFICATIONS keep every rule
   they have and are not part of the encounter; they now sit still
   instead of fading with the section. Nothing here touches the photo
   track, the slides, the dots or the marquee.
   ========================================================= */
#profile {
  --profile-portrait-lead: 90ms;   /* after the heading starts */
  --profile-content-lead: 220ms;   /* portrait + 130ms */
  --profile-portrait-rise: 18px;
}
@media (max-width: 759px) {
  #profile {
    --profile-content-lead: 200ms; /* portrait + 110ms */
    --profile-portrait-rise: 12px;
  }
}
.profile__photo-frame {
  transition:
    opacity 950ms var(--motion-ease) var(--profile-portrait-lead),
    transform 950ms var(--motion-ease) var(--profile-portrait-lead);
}
.profile__body {
  transition:
    opacity 800ms var(--motion-ease) var(--profile-content-lead),
    transform 800ms var(--motion-ease) var(--profile-content-lead);
}
/* PRE-STATES: only behind the M1 gate, only while the claimed heading waits. */
html.has-motion-observer #profile > .section__head[data-motion-state="wait"] ~ .profile__grid > .profile__photo-frame {
  opacity: 0;
  transform: translate3d(0, var(--profile-portrait-rise), 0) scale(0.985);
  transition: none;
}
html.has-motion-observer #profile > .section__head[data-motion-state="wait"] ~ .profile__grid > .profile__body {
  opacity: 0;
  transform: translate3d(0, 10px, 0);
  transition: none;
}
#profile.reveal,
#profile.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
  transition: none;
}
@media (prefers-reduced-motion: reduce) {
  html.has-motion-observer #profile > .section__head[data-motion-state="wait"] ~ .profile__grid > .profile__photo-frame,
  html.has-motion-observer #profile > .section__head[data-motion-state="wait"] ~ .profile__grid > .profile__body { opacity: 1; transform: none; }
  .profile__photo-frame,
  .profile__body { transition: none; }
}
/* =========================================================
   M3.1 QUALIFICATIONS MOTION — 「情報が静かに整って、読める状態になる」 (★★☆☆☆)
   The quiet bridge between the photo marquee and NEWS. Same mechanism as
   M2/M3: the title is the one element the M1 observer watches, on the
   foundation's quietest kind (block: opacity, 6px, 600ms, no blur, entry-
   unlocked like the PROFILE head); the ten items follow its state by CSS.
   Item n settles at T0 + 110ms + 50ms * min(n, 4) with opacity + 5px over
   500ms, no blur, no scale; the cap keeps the accumulated stagger at
   200ms, so the whole encounter is done ~810ms after the title starts.
   DOM order is the reading order in both layouts (single column <=759,
   row-major two-column pairs from 760). The list has no transition of its
   own, so the transition here replaces nothing. The marquee above and the
   PROFILE grid keep their rules untouched; the retired #profile section
   fade stays retired (M3 decision).
   ========================================================= */
#profile {
  --quals-lead: 110ms;     /* first item after the title starts */
  --quals-stagger: 50ms;
}
.profile__qualifications-list > li { --quals-i: 0; }
.profile__qualifications-list > li:nth-child(2) { --quals-i: 1; }
.profile__qualifications-list > li:nth-child(3) { --quals-i: 2; }
.profile__qualifications-list > li:nth-child(4) { --quals-i: 3; }
.profile__qualifications-list > li:nth-child(n+5) { --quals-i: 4; } /* cap: item 5 and every later item */
.profile__qualifications-list > li {
  --quals-delay: calc(var(--quals-lead, 0ms) + var(--quals-i, 0) * var(--quals-stagger, 0ms));
  transition:
    opacity 500ms var(--motion-ease) var(--quals-delay),
    transform 500ms var(--motion-ease) var(--quals-delay);
}
/* PRE-STATE: only behind the M1 gate, only while the claimed title waits. */
html.has-motion-observer .profile__qualifications > .profile__qualifications-title[data-motion-state="wait"] ~ .profile__qualifications-list > li {
  opacity: 0;
  transform: translate3d(0, 5px, 0);
  transition: none; /* entering the pre-state is instant; only the way out animates */
}
@media (prefers-reduced-motion: reduce) {
  html.has-motion-observer .profile__qualifications > .profile__qualifications-title[data-motion-state="wait"] ~ .profile__qualifications-list > li { opacity: 1; transform: none; }
  .profile__qualifications-list > li { transition: none; }
}
/* =========================================================
   M4 WORKS MOTION — 「展示空間に作品が揃っていく」 (★★★★★)
   The exhibition comes into focus. Same mechanism as M2/M3/M3.1: the
   WORKS heading is the one element the M1 observer watches (kind
   heading, untouched); the showcase cards follow its state by CSS. The
   cards never leave: they wait already present (opacity .48, scale .985,
   blur 2px, no translate) and resolve over 680ms on the site ease --
   LEFT at T0+150ms, CENTER +120ms, RIGHT +120ms (DOM order is the rail's
   visual order, left to right), so the last card settles ~1.07s after
   the heading starts. Only the initial showcase takes part: from 760 the
   first three real cards (the rail shows ~2 at 760, 3 from ~1100), at
   <=759 the one centred real card (its neighbours are 15% slivers and
   the loop clone; they stay still). Nothing else is animated -- no
   clone, no off-screen card, no ARCHIVE row.
   OWNERSHIP. The motion sits on the <article> itself, which owns no
   transform, transition or filter of its own: the TITLE/CHARACTER
   crossfade (opacity on .work-item__frame, scale on the landscape-role
   mat), the hover dim on .work-item__photo, the tab switch fade on
   #works-grid and the mascot tutorial (a sibling of the grid, absolute
   in the section) are all other elements and other properties. The
   final state is the default (no transform, no filter), so nothing is
   left on the card for a later crossfade to fight.
   ONCE. The heading is claimed once; a re-render of the showcase (ALL
   tab, breakpoint resync) after it is "in" produces final cards -- no
   replay, no re-blur. The section fade (.reveal) is retired for #works
   as for #news / #profile so the focus is not stacked on an 18px rise
   (translateY(0) kept: stacking context for the z-index:-1 leaf decor).
   Gate: no JS / no IntersectionObserver / no claim -> final cards;
   reduced motion -> final at once. JS unchanged.
   ========================================================= */
#works {
  --works-lead: 150ms;     /* first card after the heading starts */
  --works-stagger: 120ms;
  --works-dur: 680ms;
  --works-from: 0.48;
  --works-scale: 0.985;
  --works-blur: 2px;
}
.works-grid > .work-item { --works-i: 0; }
.works-grid > .work-item:nth-child(2) { --works-i: 1; }
.works-grid > .work-item:nth-child(3) { --works-i: 2; }
.works-grid > .work-item--clone:first-child + .work-item { --works-i: 0; } /* <=759: the loop clone precedes the first real card */
.works-grid > .work-item {
  --works-delay: calc(var(--works-lead, 0ms) + var(--works-i, 0) * var(--works-stagger, 0ms));
  transition:
    opacity var(--works-dur) var(--motion-ease) var(--works-delay),
    transform var(--works-dur) var(--motion-ease) var(--works-delay),
    filter var(--works-dur) var(--motion-ease) var(--works-delay);
}
/* PRE-STATE: only behind the M1 gate, only while the claimed heading waits,
   only on the initial showcase. */
@media (min-width: 760px) {
  html.has-motion-observer #works > .section__head[data-motion-state="wait"] ~ .works-grid > .work-item:nth-child(-n+3):not(.work-item--clone) {
    opacity: var(--works-from);
    transform: scale(var(--works-scale));
    filter: blur(var(--works-blur));
    transition: none; /* entering the pre-state is instant; only the way out animates */
  }
}
@media (max-width: 759px) {
  html.has-motion-observer #works > .section__head[data-motion-state="wait"] ~ .works-grid > .work-item:first-child:not(.work-item--clone),
  html.has-motion-observer #works > .section__head[data-motion-state="wait"] ~ .works-grid > .work-item--clone:first-child + .work-item {
    opacity: var(--works-from);
    transform: scale(var(--works-scale));
    filter: blur(var(--works-blur));
    transition: none;
  }
}
#works.reveal,
#works.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
  transition: none;
}
@media (prefers-reduced-motion: reduce) {
  html.has-motion-observer #works > .section__head[data-motion-state="wait"] ~ .works-grid > .work-item { opacity: 1; transform: none; filter: none; }
  .works-grid > .work-item { transition: none; }
}
/* =========================================================
   M5 CONTACT MOTION — 「会話を始める準備が整う」 (★★★★☆, most of it
   already on the page)
   Two M1 targets, nothing else: the heading (kind heading, untouched:
   opacity, 14px, blur 6px / 4px <=759, 900ms) and the form as ONE block
   (kind block, tuned below: opacity + 8px, 700ms, no blur, no scale, no
   per-field stagger). The form is claimed on its own rather than made a
   CSS follower of the heading because at every tested width it starts
   300-380px below the fold when the heading crosses the -12% line: as a
   follower it would settle entirely off-screen. Claimed, it settles as
   it arrives, and when heading and form enter together (a jump to the
   section) the foundation's batch order gives it the 180ms lead below.
   The personal-message artwork keeps its own reveal untouched: it is its
   own .reveal carrier with the pre-state on the IMG behind
   html.has-reveal-observer, independent of the section's class, and the
   6s float still starts 1s after its own is-visible. The CTA keeps its
   breathe; it moves only with its parent form. The section fade
   (.reveal) is retired for #contact as for #news / #profile / #works so
   the heading focus is not stacked on an 18px rise (translateY(0) kept:
   stacking context for the z-index:-1 leaf decor). JS unchanged.
   ========================================================= */
#contact-form[data-motion] {
  --motion-dur: 700ms;
  --motion-rise: 8px;
  --motion-stagger: 0ms;   /* its batch index never adds a delay */
  --motion-delay: 180ms;   /* lead after the heading when they enter together */
}
#contact.reveal,
#contact.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
  transition: none;
}
/* =========================================================
   PATCH 006 — IMAGE INTERACTION PROTECTION (casual save deterrence)
   Goal: an ordinary visitor cannot save a photo or an artwork through the
   ordinary gestures — Desktop drag-to-desktop, iOS long-press callout,
   selection-based copy. This is DETERRENCE, not protection: screenshots,
   DevTools and the direct asset URL are all still open, by design (no DRM,
   no canvas, no overlay).

   Element selectors on purpose: every current image is covered — HERO,
   PROFILE portrait / marquee / quick-link logos, WORKS frames, clones and
   the tutorial mascot, SNS artworks, logos and marquee, CONTACT artwork,
   honey, hachikirin, the 404 mascot (404.html loads this stylesheet) — and
   so is every image a later round or the CMS adds, with no per-image
   attribute to remember. `video` covers the SITE ENTRY clip, which is
   pointer-events:none today and unaffected either way.

   NOT here, deliberately: pointer-events (it would break the inline
   <a>/<button> image hit areas and is not needed), overlays, opacity,
   position, z-index, layout, animation — visual output is byte-identical.
   user-select stays scoped to media: page text, form fields and the
   archive ledger remain selectable. Link-level -webkit-touch-callout
   (.sns-gallery__link / .profile__quicklink) is NOT applied — it would
   cost the iOS link preview, and is reopened only if real-device QA shows
   a save path surviving through a linked image (P6-2, conditional).
   The JS half (delegated contextmenu / dragstart on media targets) lives
   in js/main.js; without JS this block still stands on its own.
   ========================================================= */
img,
video {
  -webkit-user-drag: none;      /* Safari / Chrome / Edge: no native image drag */
  -webkit-touch-callout: none;  /* iOS Safari: no long-press save / copy / share callout */
  -webkit-user-select: none;
  user-select: none;            /* an image is never part of a text selection */
}
