summaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-05-07 01:22:55 +0100
committerGitHub <noreply@github.com>2026-05-07 01:22:55 +0100
commitd7048aaef8ffa483c63a765d2d35ae01389e331f (patch)
tree9d9b50d628764dd1121b1aacd713973fc0bbd394 /frontend
parent928598b1b8399a95918dc1b315274a9d175eb8d9 (diff)
downloadsoryu-d7048aaef8ffa483c63a765d2d35ae01389e331f.tar.gz
soryu-d7048aaef8ffa483c63a765d2d35ae01389e331f.zip
Replace PC-98 landing aesthetic with Heisei Twilight theme (#127)
* Replace PC-98 landing aesthetic with Heisei Twilight theme Layered new heisei.css (loaded after pc98.css) over the public landing page so the late-2000s/early-2010s anime cue lands subtly without a ground-up rewrite. Internal VN/contracts/daemons screens are untouched. What changed - Twilight gradient backdrop (deep night → dusk plum → horizon rose + amber) replaces the chunky animated GIF background. - Soft bokeh plates and a faint scanline overlay sit behind the content for atmosphere. - Floating header is now frosted glass with a 1px hairline; the system status pills use Inter-tracked uppercase labels with desaturated cyan/magenta indicators. - Vertical kanji masthead becomes a quiet Noto Serif JP column rather than an arcade box; English subtitle picks up a warm amber tone. - Issue badge is a flat pill; LED heart still pulses but in muted rose. - Empty hero state shows a faint tactical reticle (corner brackets) and bilingual JP/EN tagline — Ghost-in-the-Shell SAC HUD vibe. - Mission and MAKIMA panels render as Heisei magazine columns with a Noto Serif JP headline, accent rule (amber for Mission, magenta for MAKIMA), and Inter body copy. - Buttons are flat with hairline borders and a left-edge amber accent sweep on hover; the Login CTA keeps a warm gradient as the primary action. - Bottom Velocity/Energy strip becomes a small HUD pill with corner bracket detailing. - Mobile breakpoint tightens header, padding, headline size, and stacks the secondary CTAs back to a column. - prefers-reduced-motion respected for the heart pulse and image pan. How to verify - cd frontend && npm install && npm run dev - Compare http://localhost:5173 with the live soryu.co (PC-98 build) - Toggle the Mission and MAKIMA panels; resize to 390x844 for mobile. Implementation notes - Single new file: frontend/src/styles/heisei.css (~16 KB). - main.tsx imports heisei.css after pc98.css so the override wins. - No JSX/component changes — design lives entirely in CSS, easy to revert by removing the import line. - npm run build passes (vite v5.4, 91 kB CSS gzipped to 17.8 kB). * Attach before/after screenshots for review Six viewport/state combinations captured at 2x DPR with Playwright (desktop 1440x900, mobile 390x844). before-* show the live soryu.co PC-98 build; after-* show the heisei-twilight branch served by vite dev. Drop this commit before merge if you'd rather not carry the assets in tree. * Move CTA row into a top navbar below the floating header Per review feedback, lift Mission / MAKIMA / Login from the bottom of the cover into a fixed navbar that sits flush beneath the floating header — standard site pattern. - .cta-area is now position: fixed, top: 72px, height 52px (top: 56px, height 48px on mobile), with the same frosted-glass + hairline treatment as the header so they read as one stacked nav block. - Buttons become ghost-link items inside the bar with an animated underline-from-center on hover; Login keeps its warm amber→rose primary treatment but compact. - cover-content top padding bumped to 156px (desktop) / 124px (mobile) so the masthead and hero clear both stripes. - bottom-stats no longer needs to dodge a fixed-bottom CTA — pulled back to bottom: 24px (desktop) and ~20px + safe-area (mobile). - npm run build passes (CSS 91.6 kB / 17.9 kB gzip). Screenshots in docs/heisei-screenshots/after-*.jpg refreshed against the new navbar layout (1440x900 desktop, 390x844 mobile, 2x DPR). * Center mobile navbar buttons (kill leftover pc98 margin-top) The bottom-CTA layout in pc98.css had a mobile-specific margin-top: 20px on .taisho-cta, which carried over into the new top navbar and pushed every button 20px below the navbar's vertical center on mobile (visible misalignment at 390x844 — the Mission/MAKIMA/Login pills were sitting near the bottom edge of the bar). - Add margin: 0 !important to .taisho-cta in heisei.css (and to the mobile-block override) so the legacy column-layout margin is wiped on both breakpoints. - Switch buttons to height: 32px + line-height: 32px for stable vertical centering inside the 48px (mobile) / 52px (desktop) navbar. - Force align-self: center on .cta-left/.cta-right so they don't inherit any stretched height from previously-applied rules. Verified via Playwright DOM probe at 390x844: navbar y=56–104 (centerY 80) · buttons y=64–96 (centerY 80) ✓ And at 1280x720: navbar y=72–124 (centerY 98) · buttons y=83–113 (centerY 98) ✓ Screenshots in docs/heisei-screenshots/after-*.jpg refreshed. * Tighten mission/makima top space and use highlighted nav state The CTA row is now a top navbar, so the old 'Close' label on the active panel button is redundant — the button itself should look like the current page. Two fixes here: 1. Replace 'Close' with a persistent active highlight. - LandingPage.tsx: button keeps its label ('Mission' / 'MAKIMA') and gains an is-active class + aria-current="page" when its panel is open. heisei.css renders the active state as a solid amber underline + amber text — same visual language as the hover indicator, just persistent. - The button still toggles the panel closed when clicked again, so the interaction is unchanged; only the labelling moved. 2. Remove the white-space band above mission content. - The legacy .modern-landing-page wrapper from pc98.css pinned itself at top: 120px and height: calc(100vh - 120px), matching the original 120px-tall floating header. The new header is 72px (56px on mobile), which left ~50px of dead space below the navbar before the mission/makima content started. - Override the wrapper to top: 72px / height: calc(100vh - 72px) on desktop and 56px on mobile so it sits flush under the header. - Cover-content padding-top retuned for the new wrapper origin: desktop default 84px, mission-mode 72px (navbar 52 + 20 gap). Mobile default 76px, mission-mode 64px (navbar 48 + 16 gap). Verified at 1280x720: navbar y=72-124 · mission headline starts y=144 · 20px gap ✓ active Mission button: 'taisho-cta is-active', aria-current=page ✓ npm run build passes (CSS 92.6 kB / 18.1 kB gzip). Screenshots in docs/heisei-screenshots/after-*.jpg refreshed against the tightened layout. * Align mission/makima body, fix mobile bg, add typewriter hero rotator Three fixes coming out of QA on staging (soryu.eirin.xyz): 1. Mission / MAKIMA body alignment - The amber/magenta accent rule + 18px indent used to live on .mission-headline only, so paragraphs (and the mission image) started 18px to the LEFT of the headline. - Move the rule + padding-left onto the .mission-screen wrapper itself. Headline, image, and all paragraph(s) now share one consistent column edge. - Tighten the makima-badge so it doesn't stretch the grid column (justify-self: start + width: max-content). Image gets explicit width: 100%; display: block to behave inside the column. 2. Mobile background coverage - pc98.css mobile-block override ('@media max-width:768px') reset .modern-landing-page to position: relative + margin-top: 100px, leaving a 100-156px band at the top where only the body color showed and pushing the wrapper bottom past the viewport. - Force position: fixed on the wrapper at every breakpoint with !important. Use 100dvh (dynamic viewport height) so the gradient follows iOS Safari's collapsing URL bar rather than getting clipped. - Mirror the dusk gradient onto <body> as a fallback so any sub-pixel rounding gap shows the same colour, not white. 3. Typewriter hero rotator - New <TypewriterRotator/> component (frontend/src/components/ TypewriterRotator.tsx) — types, holds, deletes, gaps, advances. Honours prefers-reduced-motion (renders the first phrase static). - LandingPage.tsx renders it inside a new .hero-tagline wrapper where the old static CSS ::after lived. Five phrases, all bilingual JP/EN in the same Heisei tactical voice: * 低遅延ストリーミング · LOW-LATENCY OBSERVABILITY * リアルタイム監視 · REAL-TIME SURVEILLANCE * ミッションクリティカル · MISSION-CRITICAL INFRASTRUCTURE * エンドツーエンド可視化 · END-TO-END VISIBILITY * 安全な意思決定 · SECURE DECISIONS AT THE EDGE - Hide the old .hero::after pseudo-tagline; .hero-tagline keeps the same centred position, amber colour, and tracking. - Caret blink animation (steps(1), 1s) — separate from the typing timing so it always blinks at human speed. npm run build passes (CSS 94 kB / 18.2 kB gzip, JS 238 kB / 75 kB gzip — +1.5 kB net for the rotator + new phrases). Verified at 1280x720 / 390x844 via Playwright DOM probe: - mission/makima: headline.x === paragraph.x === image.x ✓ - mobile: wrapper y=56, h=788, bottom=844 (full coverage) ✓ - typewriter: types, holds, deletes, advances to phrase 2 ✓ Screenshots in docs/heisei-screenshots/after-*.jpg refreshed. Staging at soryu.eirin.xyz updated via rsync. * Center hero on homepage, image-left text-right on mission/makima - Default homepage now uses single-column grid; the SORYU masthead is absolutely positioned in the upper-left of the cover frame, so the hero typewriter is genuinely centered on the page (verified taglineCenterX === pageCenter at 1280px). - Mission/MAKIMA on desktop (>=1025px) switch to a flex layout with the image absolutely positioned in a 360px left gutter and all body text (badge -> headline -> paragraphs) flowing on the right. - Mobile and tablet (<=1024px) keep the stacked layout; masthead returns to inline flow under the navbar. - MAKIMA badge uses flex `order: -1` so it appears above the headline visually, even though the JSX renders headline first. * Replace PC-98 character image with tactical observability mesh SVG - New /public/mission-tactical.svg (~9KB, license-clean, hand-crafted): amber wireframe globe + magenta mesh links, GitS:SAC-style HUD chrome (corner brackets, scanlines, telemetry strip, NODE-001 TYO label, pulsing origin node), kanji watermark. - Uses CSS palette vars (amber #e8b87a / magenta #d96a8a / cyan #9ad7e0). - .mission-image switched to object-fit:contain with a dark fill so the vector renders fully and never crops; pc98 mission-pan animation is killed for the SVG path. * Animate mission tactical SVG: flowing data along mesh links - Each of 6 mesh arcs now has an id (#arc1..#arc6) with two layered animations: a flowing stroke-dashoffset (so the lines themselves appear to ripple), and a separate breathing opacity cycle. - 8 packet circles ride the arcs via <animateMotion> + <mpath>, with staggered begin times (0s..2.0s) and varied durations (2.6s..4.0s) so traffic feels asynchronous, not metronomic. - Amber arcs carry amber packets, magenta arcs carry magenta packets. - Hub nodes now have layered radial pulses (filled core + expanding ring) on independent timings; cyan relay dots blink gently. - REC indicator pulses 1.4s. All animations are SVG-native (no JS, no external library); browsers respect prefers-reduced-motion via UA defaults on <animate>. * chore: gitignore tsconfig.tsbuildinfo (auto-generated incremental build cache) * Drop mission-image ring border; reskin Login hover (no glow) - The animated SVG already draws its own hairline frame + corner brackets at the viewBox edge. Remove the `box-shadow: 0 0 0 1px` ring (in both the base `.mission-image` rule and the desktop-min-1025px `.mission-screen .mission-image` override) so the image isn't double-framed. Drop-shadow for elevation kept. - Replace the Login button hover effect. The amber drop-shadow halo used the same visual language as the active Mission/MAKIMA nav buttons (which also glow amber underneath); on hover Login now inverts to a deep-night background with amber ink and a hairline ::after rule that slides in from the left along the top edge — reads as 'armed' rather than 'glowing'. Letter-spacing widens slightly on hover (0.18em → 0.22em) for a subtle ink-spread. Added :active and :focus-visible states for keyboard a11y. * Remove vertical accent rule from Mission/MAKIMA text columns The 2px amber/magenta border-left + 18px indent on the right-column text was reading as another border running parallel to the SVG image on the left. The animated SVG already supplies all the framing the panels need, so the rule is now redundant noise. Removed in three places: - .mission-screen wrapper (base + .makima-screen variant) - desktop (>=1025px) per-block rule on headline + paragraph - mobile (<=768px) leftover padding-left: 14px on headline Verified: borderLeft = 0 on wrapper, headline, and paragraph(s) on both Mission and MAKIMA. Headline + body now sit flush with the badge at the same column edge (x=680 at 1280px). * Mission/MAKIMA backdrop: solid dark plate (no horizon bleed) The panel backdrop was rgba(11,17,36,0.62→0.85) — semi-transparent — so the page's dusk-pink horizon at the bottom of the gradient bled through behind the panel content, drawing the eye to the page edge and making the background gradient stand out instead of disappearing. Switched to a near-opaque #0a1024 → #0b1530 vertical gradient (slight slope kept for depth) and bumped backdrop-filter to blur(20px) saturate(80%) so any residual ambient is fully diffused. The panel now reads as its own surface rather than a tinted overlay. Verified: bottom-center pixel is rgb(10,20,47) — uniform deep night across the whole panel, no pink bleed at the page bottom. * Drop dark rounded plate behind Mission/MAKIMA images The .mission-image and .makima-logo had a semi-opaque dark fill (rgba(11,17,36,0.55-0.6)) plus border-radius: 2px. Combined with object-fit: contain (which letterboxes the SVG inside the larger 360px box), the dark fill was visible as a rounded rectangle 'frame' around the artwork — most noticeable on MAKIMA where the small circular logo sits in a much larger box. Switched both to transparent background + border-radius: 0 so the artwork floats directly on the panel backdrop. Drop-shadow kept on both for elevation; .makima-logo's amber halo + hairline ring removed too (it was painting the same noise). Verified by sampling pixels at the four image corners + inner mid: all read as the same uniform deep-night as the panel backdrop (rgb(10, 14-18, 30-42)). No plate, no rounded edge. * Remove box-shadow halo around Mission/MAKIMA image bounding boxes * Mission/MAKIMA backdrop: slightly translucent (alpha 0.72→0.84, blur 28px) --------- Co-authored-by: soryu-co <bot@soryu.co>
Diffstat (limited to 'frontend')
-rw-r--r--frontend/.gitignore1
-rw-r--r--frontend/public/mission-tactical.svg335
-rw-r--r--frontend/src/components/LandingPage.tsx57
-rw-r--r--frontend/src/components/TypewriterRotator.tsx84
-rw-r--r--frontend/src/main.tsx1
-rw-r--r--frontend/src/styles/heisei.css836
-rw-r--r--frontend/tsconfig.tsbuildinfo1
7 files changed, 1296 insertions, 19 deletions
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..43370fa
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1 @@
+*.tsbuildinfo
diff --git a/frontend/public/mission-tactical.svg b/frontend/public/mission-tactical.svg
new file mode 100644
index 0000000..342af2a
--- /dev/null
+++ b/frontend/public/mission-tactical.svg
@@ -0,0 +1,335 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" role="img" aria-label="Tactical observability mesh"
+ font-family="'JetBrains Mono', 'Inter', monospace">
+ <defs>
+ <!-- Background gradient: deep dusk -> night -->
+ <radialGradient id="bgGlow" cx="50%" cy="55%" r="65%">
+ <stop offset="0%" stop-color="#1a2548"/>
+ <stop offset="55%" stop-color="#0e1530"/>
+ <stop offset="100%" stop-color="#070a18"/>
+ </radialGradient>
+ <!-- Globe fill: very subtle inner depth -->
+ <radialGradient id="globeFill" cx="38%" cy="36%" r="65%">
+ <stop offset="0%" stop-color="rgba(232,184,122,0.08)"/>
+ <stop offset="60%" stop-color="rgba(217,106,138,0.04)"/>
+ <stop offset="100%" stop-color="rgba(11,17,36,0)"/>
+ </radialGradient>
+ <!-- Soft glow for active nodes -->
+ <radialGradient id="nodeGlow">
+ <stop offset="0%" stop-color="#e8b87a" stop-opacity="0.85"/>
+ <stop offset="60%" stop-color="#e8b87a" stop-opacity="0.10"/>
+ <stop offset="100%" stop-color="#e8b87a" stop-opacity="0"/>
+ </radialGradient>
+ <radialGradient id="nodeMag">
+ <stop offset="0%" stop-color="#d96a8a" stop-opacity="0.95"/>
+ <stop offset="60%" stop-color="#d96a8a" stop-opacity="0.10"/>
+ <stop offset="100%" stop-color="#d96a8a" stop-opacity="0"/>
+ </radialGradient>
+ <!-- Scanline pattern -->
+ <pattern id="scan" width="2" height="3" patternUnits="userSpaceOnUse">
+ <rect width="2" height="3" fill="rgba(0,0,0,0)"/>
+ <rect width="2" height="1" fill="rgba(240,234,223,0.025)"/>
+ </pattern>
+ <!-- Pulse animation for the central reticle -->
+ <filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="1.6" result="b"/>
+ <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
+ </filter>
+ </defs>
+
+ <!-- Backdrop -->
+ <rect width="600" height="600" fill="url(#bgGlow)"/>
+ <rect width="600" height="600" fill="url(#scan)"/>
+
+ <!-- Outer hairline frame + corner brackets (matches Heisei UI language) -->
+ <g stroke="rgba(240,234,223,0.55)" fill="none" stroke-width="1">
+ <!-- corner brackets, 18px legs -->
+ <path d="M14,14 L14,32 M14,14 L32,14"/>
+ <path d="M586,14 L568,14 M586,14 L586,32"/>
+ <path d="M14,586 L14,568 M14,586 L32,586"/>
+ <path d="M586,586 L568,586 M586,586 L586,568"/>
+ </g>
+ <rect x="14" y="14" width="572" height="572" fill="none"
+ stroke="rgba(240,234,223,0.10)" stroke-width="0.5"/>
+
+ <!-- Tactical header strip -->
+ <g font-size="9" letter-spacing="2.5" fill="rgba(240,234,223,0.55)">
+ <text x="32" y="40">SORYU // OBSERVABILITY MESH</text>
+ <text x="568" y="40" text-anchor="end" fill="#e8b87a">REC <tspan fill="#d96a8a">●</tspan>
+ <animate attributeName="opacity" values="1;0.4;1" dur="1.4s" repeatCount="indefinite"/>
+ </text>
+ </g>
+ <g font-size="8" letter-spacing="1.8" fill="rgba(240,234,223,0.40)">
+ <text x="32" y="54">35.6764°N · 139.6500°E</text>
+ <text x="568" y="54" text-anchor="end">v1.0.0 / LIVE</text>
+ </g>
+
+ <!-- Faint grid behind the globe -->
+ <g stroke="rgba(154,215,224,0.08)" stroke-width="0.5" fill="none">
+ <line x1="60" y1="300" x2="540" y2="300"/>
+ <line x1="300" y1="60" x2="300" y2="540"/>
+ </g>
+
+ <!-- ============ GLOBE ============ -->
+ <!-- Centered at (300,320), radius 200 -->
+ <g transform="translate(300 320)">
+ <!-- Filled disc -->
+ <circle r="200" fill="url(#globeFill)"/>
+ <!-- Equator + main rings (orthographic projection) -->
+ <g fill="none" stroke="rgba(232,184,122,0.55)" stroke-width="1.1">
+ <circle r="200"/>
+ </g>
+ <!-- Latitude ellipses (front-only effect via stroke-dasharray on back) -->
+ <g fill="none" stroke="rgba(232,184,122,0.42)" stroke-width="0.7">
+ <ellipse rx="200" ry="170"/>
+ <ellipse rx="200" ry="120"/>
+ <ellipse rx="200" ry="60"/>
+ <ellipse rx="200" ry="0.4"/> <!-- equator line emphasis -->
+ </g>
+ <!-- Longitude ellipses (rotated) -->
+ <g fill="none" stroke="rgba(232,184,122,0.42)" stroke-width="0.7">
+ <ellipse rx="60" ry="200"/>
+ <ellipse rx="120" ry="200"/>
+ <ellipse rx="170" ry="200"/>
+ </g>
+ <!-- Tilted accent rings for depth -->
+ <g fill="none" stroke="rgba(217,106,138,0.55)" stroke-width="0.9"
+ transform="rotate(-22)">
+ <ellipse rx="200" ry="80"/>
+ </g>
+ <g fill="none" stroke="rgba(154,215,224,0.45)" stroke-width="0.7"
+ transform="rotate(28)">
+ <ellipse rx="200" ry="40"/>
+ </g>
+
+ <!-- ============ NODES + MESH LINKS ============ -->
+ <!-- Coordinates roughly evoke continents on a tilted globe -->
+ <g>
+ <!-- Mesh links: each path has an id so animated pulses can ride along it.
+ The arc itself shows a flowing dash pattern, and a glowing dot
+ travels along it via <animateMotion>. -->
+ <g fill="none" stroke-linecap="round">
+ <!-- arc 1 (amber) NODE-A → TYO -->
+ <path id="arc1" d="M -120,-70 Q -40,-180 60,-90"
+ stroke="rgba(232,184,122,0.55)" stroke-width="0.9"
+ stroke-dasharray="2 6" stroke-dashoffset="0" filter="url(#softGlow)">
+ <animate attributeName="stroke-dashoffset" from="0" to="-32" dur="1.6s" repeatCount="indefinite"/>
+ <animate attributeName="opacity" values="0.65;1;0.65" dur="2.6s" repeatCount="indefinite"/>
+ </path>
+ <!-- arc 2 (amber) TYO → SE -->
+ <path id="arc2" d="M 60,-90 Q 130,-30 150, 60"
+ stroke="rgba(232,184,122,0.55)" stroke-width="0.9"
+ stroke-dasharray="2 6" filter="url(#softGlow)">
+ <animate attributeName="stroke-dashoffset" from="0" to="-32" dur="1.9s" repeatCount="indefinite"/>
+ <animate attributeName="opacity" values="0.65;1;0.65" dur="2.9s" repeatCount="indefinite"/>
+ </path>
+ <!-- arc 3 (amber) SE → SW -->
+ <path id="arc3" d="M 150, 60 Q 60,150 -30,130"
+ stroke="rgba(232,184,122,0.55)" stroke-width="0.9"
+ stroke-dasharray="2 6" filter="url(#softGlow)">
+ <animate attributeName="stroke-dashoffset" from="0" to="-32" dur="2.1s" repeatCount="indefinite"/>
+ <animate attributeName="opacity" values="0.6;0.95;0.6" dur="3.1s" repeatCount="indefinite"/>
+ </path>
+ <!-- arc 4 (amber) SW → NW (closes ring) -->
+ <path id="arc4" d="M -30,130 Q -120,80 -120,-70"
+ stroke="rgba(232,184,122,0.55)" stroke-width="0.9"
+ stroke-dasharray="2 6" filter="url(#softGlow)">
+ <animate attributeName="stroke-dashoffset" from="0" to="-32" dur="1.7s" repeatCount="indefinite"/>
+ <animate attributeName="opacity" values="0.6;0.9;0.6" dur="2.7s" repeatCount="indefinite"/>
+ </path>
+ <!-- arc 5 (magenta) NW → SE diagonal -->
+ <path id="arc5" d="M -120,-70 Q 0,-30 150, 60"
+ stroke="rgba(217,106,138,0.55)" stroke-width="0.9"
+ stroke-dasharray="3 7" filter="url(#softGlow)">
+ <animate attributeName="stroke-dashoffset" from="0" to="-40" dur="2.4s" repeatCount="indefinite"/>
+ <animate attributeName="opacity" values="0.55;0.95;0.55" dur="3.4s" repeatCount="indefinite"/>
+ </path>
+ <!-- arc 6 (magenta) TYO → SW diagonal -->
+ <path id="arc6" d="M 60,-90 Q -10, 60 -30,130"
+ stroke="rgba(217,106,138,0.45)" stroke-width="0.8"
+ stroke-dasharray="3 7" filter="url(#softGlow)">
+ <animate attributeName="stroke-dashoffset" from="0" to="-40" dur="2.8s" repeatCount="indefinite"/>
+ <animate attributeName="opacity" values="0.5;0.9;0.5" dur="3.8s" repeatCount="indefinite"/>
+ </path>
+ </g>
+
+ <!-- Traveling data packets (glowing dots ride each arc).
+ Each packet is a tiny circle with <animateMotion> referencing
+ the arc path via <mpath>. Stagger begin times so packets feel asynchronous. -->
+ <g>
+ <!-- amber packets -->
+ <circle r="2.4" fill="#e8b87a" filter="url(#softGlow)">
+ <animateMotion dur="2.8s" repeatCount="indefinite" rotate="auto" begin="0s">
+ <mpath href="#arc1"/>
+ </animateMotion>
+ <animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.1;0.9;1" dur="2.8s" repeatCount="indefinite"/>
+ </circle>
+ <circle r="2.0" fill="#e8b87a" filter="url(#softGlow)">
+ <animateMotion dur="2.8s" repeatCount="indefinite" rotate="auto" begin="1.2s">
+ <mpath href="#arc1"/>
+ </animateMotion>
+ <animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.1;0.9;1" dur="2.8s" repeatCount="indefinite" begin="1.2s"/>
+ </circle>
+
+ <circle r="2.4" fill="#e8b87a" filter="url(#softGlow)">
+ <animateMotion dur="3.2s" repeatCount="indefinite" rotate="auto" begin="0.4s">
+ <mpath href="#arc2"/>
+ </animateMotion>
+ <animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.08;0.92;1" dur="3.2s" repeatCount="indefinite" begin="0.4s"/>
+ </circle>
+
+ <circle r="2.4" fill="#e8b87a" filter="url(#softGlow)">
+ <animateMotion dur="3.0s" repeatCount="indefinite" rotate="auto" begin="0.9s">
+ <mpath href="#arc3"/>
+ </animateMotion>
+ <animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.1;0.9;1" dur="3.0s" repeatCount="indefinite" begin="0.9s"/>
+ </circle>
+
+ <circle r="2.4" fill="#e8b87a" filter="url(#softGlow)">
+ <animateMotion dur="2.6s" repeatCount="indefinite" rotate="auto" begin="1.6s">
+ <mpath href="#arc4"/>
+ </animateMotion>
+ <animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.1;0.9;1" dur="2.6s" repeatCount="indefinite" begin="1.6s"/>
+ </circle>
+
+ <!-- magenta packets (longer arcs, brighter core) -->
+ <circle r="2.6" fill="#d96a8a" filter="url(#softGlow)">
+ <animateMotion dur="3.6s" repeatCount="indefinite" rotate="auto" begin="0.2s">
+ <mpath href="#arc5"/>
+ </animateMotion>
+ <animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.08;0.92;1" dur="3.6s" repeatCount="indefinite" begin="0.2s"/>
+ </circle>
+ <circle r="2.0" fill="#d96a8a" filter="url(#softGlow)">
+ <animateMotion dur="3.6s" repeatCount="indefinite" rotate="auto" begin="2.0s">
+ <mpath href="#arc5"/>
+ </animateMotion>
+ <animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.08;0.92;1" dur="3.6s" repeatCount="indefinite" begin="2.0s"/>
+ </circle>
+
+ <circle r="2.4" fill="#d96a8a" filter="url(#softGlow)">
+ <animateMotion dur="4.0s" repeatCount="indefinite" rotate="auto" begin="1.0s">
+ <mpath href="#arc6"/>
+ </animateMotion>
+ <animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.08;0.92;1" dur="4.0s" repeatCount="indefinite" begin="1.0s"/>
+ </circle>
+ </g>
+
+ <!-- Active nodes (amber primary) -->
+ <g>
+ <!-- Tokyo (origin / pulse) -->
+ <circle cx="60" cy="-90" r="14" fill="url(#nodeGlow)"/>
+ <circle cx="60" cy="-90" r="3" fill="#e8b87a"/>
+ <circle cx="60" cy="-90" r="3" fill="none" stroke="#e8b87a" stroke-width="1">
+ <animate attributeName="r" values="3;18;3" dur="3s" repeatCount="indefinite"/>
+ <animate attributeName="opacity" values="1;0;1" dur="3s" repeatCount="indefinite"/>
+ </circle>
+
+ <!-- Other nodes — subtle independent pulses -->
+ <circle cx="-120" cy="-70" r="10" fill="url(#nodeGlow)"/>
+ <circle cx="-120" cy="-70" r="2.4" fill="#e8b87a">
+ <animate attributeName="opacity" values="0.7;1;0.7" dur="2.2s" repeatCount="indefinite"/>
+ </circle>
+ <circle cx="-120" cy="-70" r="2.4" fill="none" stroke="#e8b87a" stroke-width="0.8">
+ <animate attributeName="r" values="2.4;13;2.4" dur="3.4s" repeatCount="indefinite" begin="0.7s"/>
+ <animate attributeName="opacity" values="0.9;0;0.9" dur="3.4s" repeatCount="indefinite" begin="0.7s"/>
+ </circle>
+
+ <circle cx="150" cy="60" r="12" fill="url(#nodeMag)"/>
+ <circle cx="150" cy="60" r="2.6" fill="#d96a8a">
+ <animate attributeName="opacity" values="0.7;1;0.7" dur="2.8s" repeatCount="indefinite" begin="0.4s"/>
+ </circle>
+ <circle cx="150" cy="60" r="2.6" fill="none" stroke="#d96a8a" stroke-width="0.8">
+ <animate attributeName="r" values="2.6;14;2.6" dur="3.6s" repeatCount="indefinite" begin="1.4s"/>
+ <animate attributeName="opacity" values="0.9;0;0.9" dur="3.6s" repeatCount="indefinite" begin="1.4s"/>
+ </circle>
+
+ <circle cx="-30" cy="130" r="9" fill="url(#nodeGlow)"/>
+ <circle cx="-30" cy="130" r="2.2" fill="#e8b87a">
+ <animate attributeName="opacity" values="0.7;1;0.7" dur="2.4s" repeatCount="indefinite" begin="1.1s"/>
+ </circle>
+
+ <!-- Smaller relays — gentle blink so they feel alive -->
+ <g fill="#9ad7e0">
+ <circle cx="-160" cy="40" r="1.6">
+ <animate attributeName="opacity" values="0.4;1;0.4" dur="2.0s" repeatCount="indefinite" begin="0.0s"/>
+ </circle>
+ <circle cx="20" cy="-160" r="1.6">
+ <animate attributeName="opacity" values="0.4;1;0.4" dur="2.4s" repeatCount="indefinite" begin="0.5s"/>
+ </circle>
+ <circle cx="170" cy="-30" r="1.6">
+ <animate attributeName="opacity" values="0.4;1;0.4" dur="2.8s" repeatCount="indefinite" begin="1.0s"/>
+ </circle>
+ <circle cx="-80" cy="170" r="1.6">
+ <animate attributeName="opacity" values="0.4;1;0.4" dur="2.2s" repeatCount="indefinite" begin="1.5s"/>
+ </circle>
+ <circle cx="100" cy="160" r="1.6">
+ <animate attributeName="opacity" values="0.4;1;0.4" dur="2.6s" repeatCount="indefinite" begin="2.0s"/>
+ </circle>
+ </g>
+ </g>
+
+ <!-- Node label (Tokyo origin) -->
+ <g font-size="7.5" letter-spacing="1.5" fill="rgba(240,234,223,0.78)">
+ <line x1="60" y1="-90" x2="120" y2="-130" stroke="rgba(232,184,122,0.55)" stroke-width="0.6"/>
+ <text x="124" y="-127" fill="#e8b87a">NODE-001 · TYO</text>
+ <text x="124" y="-117">42ms · 99.998%</text>
+ </g>
+ </g>
+
+ <!-- Concentric reticle around the globe -->
+ <g fill="none" stroke="rgba(232,184,122,0.30)" stroke-width="0.6" stroke-dasharray="2 4">
+ <circle r="225"/>
+ </g>
+ <g fill="none" stroke="rgba(232,184,122,0.18)" stroke-width="0.5">
+ <circle r="245"/>
+ </g>
+ </g>
+
+ <!-- ============ HUD CROSSHAIR (centered) ============ -->
+ <g stroke="rgba(232,184,122,0.55)" stroke-width="0.8" fill="none">
+ <line x1="300" y1="100" x2="300" y2="130"/>
+ <line x1="300" y1="510" x2="300" y2="540"/>
+ <line x1="80" y1="320" x2="110" y2="320"/>
+ <line x1="490" y1="320" x2="520" y2="320"/>
+ </g>
+
+ <!-- ============ Bottom telemetry strip ============ -->
+ <g transform="translate(0 555)">
+ <!-- Mini bar chart -->
+ <g fill="#9ad7e0" opacity="0.7">
+ <rect x="32" y="0" width="3" height="14"/>
+ <rect x="38" y="4" width="3" height="10"/>
+ <rect x="44" y="-2" width="3" height="16"/>
+ <rect x="50" y="2" width="3" height="12"/>
+ <rect x="56" y="-4" width="3" height="18"/>
+ <rect x="62" y="0" width="3" height="14"/>
+ <rect x="68" y="-3" width="3" height="17"/>
+ <rect x="74" y="1" width="3" height="13"/>
+ <rect x="80" y="-5" width="3" height="19"/>
+ <rect x="86" y="-1" width="3" height="15"/>
+ <rect x="92" y="3" width="3" height="11"/>
+ <rect x="98" y="-6" width="3" height="20"/>
+ <rect x="104" y="-2" width="3" height="16"/>
+ <rect x="110" y="0" width="3" height="14"/>
+ </g>
+ <text x="32" y="32" font-size="8" letter-spacing="1.6" fill="rgba(240,234,223,0.45)">TELEMETRY · 1m</text>
+
+ <!-- Stat blocks -->
+ <g font-size="9" letter-spacing="1.6" fill="rgba(240,234,223,0.7)">
+ <text x="240" y="6" fill="rgba(154,215,224,0.85)">VELOCITY</text>
+ <text x="240" y="22" fill="#f0eadf" font-size="14" letter-spacing="0.5">603 km/h</text>
+
+ <text x="380" y="6" fill="rgba(154,215,224,0.85)">ENERGY</text>
+ <text x="380" y="22" fill="#f0eadf" font-size="14" letter-spacing="0.5">32.0 MJ</text>
+
+ <text x="500" y="6" fill="rgba(154,215,224,0.85)">UPTIME</text>
+ <text x="500" y="22" fill="#e8b87a" font-size="14" letter-spacing="0.5">99.998%</text>
+ </g>
+ </g>
+
+ <!-- Subtle vertical kanji watermark on the right -->
+ <g font-family="'Noto Serif JP', serif" font-size="11" letter-spacing="6"
+ fill="rgba(240,234,223,0.18)" writing-mode="tb">
+ <text x="572" y="120">観測</text>
+ </g>
+</svg>
diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx
index f126d6f..2c543a6 100644
--- a/frontend/src/components/LandingPage.tsx
+++ b/frontend/src/components/LandingPage.tsx
@@ -1,6 +1,15 @@
import React, { useState, useEffect, useRef } from 'react'
import { LoadingScreen } from './LoadingScreen'
import { HeartLogo } from './HeartLogo'
+import { TypewriterRotator } from './TypewriterRotator'
+
+const HERO_PHRASES = [
+ '低遅延ストリーミング · LOW-LATENCY OBSERVABILITY',
+ 'リアルタイム監視 · REAL-TIME SURVEILLANCE',
+ 'ミッションクリティカル · MISSION-CRITICAL INFRASTRUCTURE',
+ 'エンドツーエンド可視化 · END-TO-END VISIBILITY',
+ '安全な意思決定 · SECURE DECISIONS AT THE EDGE',
+]
interface LandingPageProps {
onLogin: () => void
@@ -167,7 +176,7 @@ export function LandingPage({ onLogin }: LandingPageProps) {
{activePanel === 'mission' ? (
<div className="mission-screen" role="region" aria-label="Mission">
<h1 className="mission-headline">Building real‑time systems for mission-critical observability and surveillance </h1>
- <img src="/PC98Doukuusei.webp" alt="Mission montage" className="mission-image" />
+ <img src="/mission-tactical.svg" alt="Tactical observability mesh" className="mission-image" />
<p className="mission-paragraph">
We deliver low‑latency streaming & infrastructure that turns live data into
reliable, secure insight. Target selection, monitoring and full end to end observability
@@ -189,27 +198,39 @@ export function LandingPage({ onLogin }: LandingPageProps) {
</p>
</div>
) : (
- <div className="hero" />
+ <div className="hero">
+ <div className="hero-tagline" aria-label="Soryu capabilities">
+ <TypewriterRotator phrases={HERO_PHRASES} />
+ </div>
+ </div>
)}
- {/* CTA row spanning full width: left Mission/MAKIMA, right Login */}
- <div className="cta-area">
- <div className="cta-left">
- <button className="taisho-cta" onClick={handleMission}>
- <span className="cta-text">{activePanel === 'mission' ? 'Close' : 'Mission'}</span>
- </button>
- <button className="taisho-cta" onClick={handleMakimaPanel}>
- <span className="cta-text">{activePanel === 'makima' ? 'Close' : 'MAKIMA'}</span>
- </button>
- </div>
- <div className="cta-right">
- <button className="taisho-cta" onClick={handleLogin}>
- <span className="cta-icon">▶</span>
- <span className="cta-text">Login</span>
- </button>
- </div>
+ {/* CTA row spanning full width: left Mission/MAKIMA, right Login */}
+ <div className="cta-area">
+ <div className="cta-left">
+ <button
+ className={`taisho-cta${activePanel === 'mission' ? ' is-active' : ''}`}
+ aria-current={activePanel === 'mission' ? 'page' : undefined}
+ onClick={handleMission}
+ >
+ <span className="cta-text">Mission</span>
+ </button>
+ <button
+ className={`taisho-cta${activePanel === 'makima' ? ' is-active' : ''}`}
+ aria-current={activePanel === 'makima' ? 'page' : undefined}
+ onClick={handleMakimaPanel}
+ >
+ <span className="cta-text">MAKIMA</span>
+ </button>
+ </div>
+ <div className="cta-right">
+ <button className="taisho-cta" onClick={handleLogin}>
+ <span className="cta-icon">▶</span>
+ <span className="cta-text">Login</span>
+ </button>
</div>
</div>
+ </div>
</div>
{/* Bottom stats: Velocity + Energy only (hidden in mission mode) */}
diff --git a/frontend/src/components/TypewriterRotator.tsx b/frontend/src/components/TypewriterRotator.tsx
new file mode 100644
index 0000000..4cd89fa
--- /dev/null
+++ b/frontend/src/components/TypewriterRotator.tsx
@@ -0,0 +1,84 @@
+import React, { useEffect, useRef, useState } from 'react'
+
+/**
+ * TypewriterRotator
+ *
+ * Cycles through a list of phrases with a typing-and-deleting effect.
+ * Designed for the soryu landing hero — sits inside a centred container,
+ * so the caller controls position and the component just renders text.
+ *
+ * Honours `prefers-reduced-motion` by rendering the first phrase only,
+ * statically.
+ */
+
+interface TypewriterRotatorProps {
+ phrases: string[]
+ /** ms per character while typing */
+ typeMs?: number
+ /** ms per character while deleting */
+ deleteMs?: number
+ /** ms to hold a fully-typed phrase before deleting */
+ holdMs?: number
+ /** ms to wait after deletion before the next phrase types in */
+ gapMs?: number
+ className?: string
+}
+
+export function TypewriterRotator({
+ phrases,
+ typeMs = 55,
+ deleteMs = 28,
+ holdMs = 2200,
+ gapMs = 320,
+ className,
+}: TypewriterRotatorProps) {
+ const [text, setText] = useState('')
+ const [index, setIndex] = useState(0)
+ const [phase, setPhase] = useState<'typing' | 'holding' | 'deleting' | 'gap'>('typing')
+ const reduceMotion = useRef(
+ typeof window !== 'undefined' &&
+ window.matchMedia &&
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches,
+ )
+
+ useEffect(() => {
+ if (reduceMotion.current) {
+ setText(phrases[0] ?? '')
+ return
+ }
+ const current = phrases[index % phrases.length] ?? ''
+ let timer: number
+
+ if (phase === 'typing') {
+ if (text.length < current.length) {
+ timer = window.setTimeout(() => setText(current.slice(0, text.length + 1)), typeMs)
+ } else {
+ timer = window.setTimeout(() => setPhase('holding'), 0)
+ }
+ } else if (phase === 'holding') {
+ // Skip the hold-then-delete cycle entirely if there's only one phrase
+ if (phrases.length <= 1) return
+ timer = window.setTimeout(() => setPhase('deleting'), holdMs)
+ } else if (phase === 'deleting') {
+ if (text.length > 0) {
+ timer = window.setTimeout(() => setText(current.slice(0, text.length - 1)), deleteMs)
+ } else {
+ timer = window.setTimeout(() => setPhase('gap'), 0)
+ }
+ } else if (phase === 'gap') {
+ timer = window.setTimeout(() => {
+ setIndex((i) => (i + 1) % phrases.length)
+ setPhase('typing')
+ }, gapMs)
+ }
+
+ return () => window.clearTimeout(timer)
+ }, [text, phase, index, phrases, typeMs, deleteMs, holdMs, gapMs])
+
+ return (
+ <span className={className} role="status" aria-live="polite">
+ <span className="tw-text">{text}</span>
+ <span className="tw-caret" aria-hidden="true" />
+ </span>
+ )
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 04b8cde..ba2e16b 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -9,6 +9,7 @@ import { DaemonList } from './components/DaemonList'
import { DaemonDetail } from './components/DaemonDetail'
import './styles/pc98.css'
import './styles/mobile.css'
+import './styles/heisei.css'
// Route configuration:
// - /contracts - List all contracts
diff --git a/frontend/src/styles/heisei.css b/frontend/src/styles/heisei.css
new file mode 100644
index 0000000..b532c02
--- /dev/null
+++ b/frontend/src/styles/heisei.css
@@ -0,0 +1,836 @@
+/* ================================================================
+ Heisei Twilight — landing page theme override
+ ----------------------------------------------------------------
+ Subtle late-Heisei (early 2000s–2010s) anime aesthetic for the
+ public landing screen. Inspired by Makoto Shinkai dusk skies,
+ GitS: SAC tactical UI hairlines, and CLAMP-era plate gradients.
+
+ Scoped to `.modern-landing-page` and the floating header so the
+ internal VN/contracts/daemons screens are untouched.
+
+ Loaded AFTER pc98.css so all selectors here win the cascade.
+ ================================================================ */
+
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300;400;500;700&family=Noto+Serif+JP:wght@300;400;600;700&family=Inter:wght@300;400;500;600;700&display=swap');
+
+:root {
+ --hz-night: #0b1124;
+ --hz-deep: #1a2548;
+ --hz-dusk: #4a3a6b;
+ --hz-rose: #c87a8a;
+ --hz-amber: #e8b87a;
+ --hz-paper: #f4ecdf;
+ --hz-ink: #f0eadf;
+ --hz-line: rgba(240, 234, 223, 0.18);
+ --hz-line-strong: rgba(240, 234, 223, 0.55);
+ --hz-cyan: #9ad7e0; /* desaturated tactical cyan */
+ --hz-mag: #d96a8a; /* muted heisei magenta */
+ --hz-shadow: 0 24px 60px -20px rgba(8, 10, 24, 0.65);
+}
+
+/* ----------------------------------------------------------------
+ 1. Page backdrop — replace busy GIF with twilight gradient + bokeh
+ ---------------------------------------------------------------- */
+
+body { background: var(--hz-night); color: var(--hz-ink); }
+
+/* Reset the legacy 120px top offset (was matched to the old 120px header).
+ New header is 72px / 56px on mobile, so claim the full remaining viewport. */
+.modern-landing-page {
+ position: fixed !important;
+ top: 72px !important;
+ left: 0 !important;
+ right: 0 !important;
+ width: 100vw !important;
+ height: calc(100dvh - 72px) !important;
+ margin-top: 0 !important;
+ min-height: 0 !important;
+ overflow: hidden !important;
+}
+@media (max-width: 768px) {
+ .modern-landing-page {
+ top: 56px !important;
+ height: calc(100dvh - 56px) !important;
+ }
+ /* Body fallback so any rounding gap shows the same dusk gradient
+ instead of a flash of system-default white */
+ body {
+ background:
+ radial-gradient(140% 70% at 50% 100%, rgba(232, 184, 122, 0.20) 0%, transparent 55%),
+ radial-gradient(120% 80% at 50% 0%, rgba(74, 58, 107, 0.55) 0%, transparent 60%),
+ linear-gradient(180deg, #0b1124 0%, #1a2548 35%, #3d3358 65%, #6b4d63 88%, #c87a8a 100%) !important;
+ background-attachment: fixed;
+ }
+}
+
+.modern-landing-page.manga-style {
+ background:
+ /* horizon glow */
+ radial-gradient(140% 70% at 50% 100%, rgba(232, 184, 122, 0.20) 0%, transparent 55%),
+ /* upper dusk */
+ radial-gradient(120% 80% at 50% 0%, rgba(74, 58, 107, 0.55) 0%, transparent 60%),
+ /* base sky */
+ linear-gradient(180deg, #0b1124 0%, #1a2548 35%, #3d3358 65%, #6b4d63 88%, #c87a8a 100%);
+}
+
+/* Hide the noisy background gif — twilight is the new backdrop */
+.modern-landing-page.manga-style .background-only { display: none; }
+
+/* Soft bokeh plates (very subtle) */
+.modern-landing-page.manga-style::before,
+.modern-landing-page.manga-style::after {
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ border-radius: 50%;
+ filter: blur(60px);
+ z-index: 0;
+}
+.modern-landing-page.manga-style::before {
+ width: 380px; height: 380px;
+ top: 12%; right: -80px;
+ background: radial-gradient(circle, rgba(232, 184, 122, 0.28), transparent 70%);
+}
+.modern-landing-page.manga-style::after {
+ width: 480px; height: 480px;
+ bottom: -120px; left: -120px;
+ background: radial-gradient(circle, rgba(154, 215, 224, 0.18), transparent 70%);
+}
+
+/* Faint scanlines — gentle, not CRT */
+.modern-landing-page.manga-style .taisho-cover .halftone-overlay,
+.modern-landing-page.manga-style .taisho-cover .pattern-ichimatsu,
+.modern-landing-page.manga-style .taisho-cover .paper-tone {
+ display: none;
+}
+
+.modern-landing-page.manga-style .taisho-cover {
+ color: var(--hz-ink);
+}
+.modern-landing-page.manga-style .taisho-cover::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 0;
+ background: repeating-linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.015) 0 1px,
+ transparent 1px 3px
+ );
+}
+
+/* Mission-mode backdrop — slightly translucent so the dusk ambient
+ (blue + bokeh) reads through, but alpha stays high enough at the
+ bottom that the pink horizon dissolves into deep blue. Strong blur
+ radius diffuses any residual into soft tone, never a band. */
+.modern-landing-page.manga-style .taisho-cover.mission-mode .cover-backdrop {
+ background: linear-gradient(180deg,
+ rgba(11, 17, 36, 0.72) 0%,
+ rgba(11, 17, 36, 0.78) 50%,
+ rgba(11, 17, 36, 0.84) 100%
+ );
+ backdrop-filter: blur(28px) saturate(90%);
+ -webkit-backdrop-filter: blur(28px) saturate(90%);
+}
+
+/* ----------------------------------------------------------------
+ 2. Floating header — frosted glass + hairline
+ ---------------------------------------------------------------- */
+
+.floating-header {
+ height: 72px;
+ padding: 0 32px;
+ background: linear-gradient(180deg, rgba(11, 17, 36, 0.82), rgba(11, 17, 36, 0.55));
+ backdrop-filter: blur(14px) saturate(140%);
+ -webkit-backdrop-filter: blur(14px) saturate(140%);
+ border-bottom: 1px solid var(--hz-line);
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04);
+}
+
+.floating-header .brand { top: 50%; left: 32px; transform: translateY(-50%); }
+.floating-header .brand-mark { height: 28px !important; width: auto; opacity: 0.92; }
+
+.system-info {
+ top: 50%; right: 32px;
+ transform: translateY(-50%);
+ flex-direction: row;
+ gap: 22px;
+ font-family: 'Inter', system-ui, sans-serif;
+ font-size: 11px;
+ opacity: 1;
+ letter-spacing: 0.12em;
+}
+.info-item { gap: 8px; align-items: center; }
+.info-label {
+ color: var(--hz-cyan);
+ font-family: 'Inter', system-ui, sans-serif;
+ font-size: 10px;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.18em;
+ opacity: 0.75;
+}
+.info-value {
+ color: var(--hz-ink);
+ font-family: 'Inter', system-ui, sans-serif;
+ font-size: 11px;
+ font-weight: 500;
+ letter-spacing: 0.1em;
+}
+.live-status { gap: 6px; }
+.status-dot { width: 6px; height: 6px; }
+.status-dot.standby {
+ background: var(--hz-cyan);
+ box-shadow: 0 0 8px rgba(154, 215, 224, 0.7);
+}
+.status-dot.live {
+ background: var(--hz-mag);
+ box-shadow: 0 0 8px rgba(217, 106, 138, 0.8);
+}
+
+/* ----------------------------------------------------------------
+ 3. Cover layout — breathe (top padding accounts for header + navbar)
+ ---------------------------------------------------------------- */
+
+/* Default homepage: single column so the hero is genuinely page-centered.
+ The masthead is overlaid in the top-left corner of the cover frame. */
+.modern-landing-page.manga-style .taisho-cover:not(.mission-mode) .cover-content {
+ grid-template-columns: 1fr;
+ grid-template-rows: 1fr; /* CTA is no longer in this grid */
+ grid-template-areas: "hero";
+ gap: 0;
+ position: relative; /* anchor for absolutely-positioned masthead */
+ /* wrapper is now top: 72px, so we only clear the 52px navbar + breathing */
+ padding: 84px 56px 56px;
+}
+
+/* Hero spans the whole row in default mode */
+.modern-landing-page.manga-style .taisho-cover:not(.mission-mode) .hero {
+ grid-area: hero;
+}
+
+/* Masthead: float above the hero in the upper-left corner of the frame */
+.modern-landing-page.manga-style .taisho-cover:not(.mission-mode) .masthead {
+ position: absolute;
+ top: 84px; /* matches cover-content padding-top */
+ left: 56px; /* matches cover-content padding-left */
+ z-index: 5;
+ pointer-events: auto;
+}
+
+/* Mission / MAKIMA panels: keep the 2-col grid (masthead column + hero column)
+ so the SORYU branding column doesn't overlap the panel content. */
+.modern-landing-page.manga-style .taisho-cover.mission-mode .cover-content {
+ grid-template-columns: 200px 1fr;
+ grid-template-rows: 1fr;
+ grid-template-areas: "masthead hero";
+ padding: 72px 56px 48px; /* 52 nav + 20 breathing */
+ gap: 32px;
+}
+
+@media (max-width: 1024px) {
+ /* Restore stacked layout on tablet/phone: masthead above hero, in flow */
+ .modern-landing-page.manga-style .taisho-cover:not(.mission-mode) .cover-content {
+ grid-template-columns: 1fr;
+ grid-template-rows: auto 1fr;
+ grid-template-areas: "masthead" "hero";
+ padding: 76px 24px 40px; /* 48 nav + 28 breathing */
+ gap: 28px;
+ }
+ .modern-landing-page.manga-style .taisho-cover:not(.mission-mode) .masthead {
+ position: static;
+ top: auto;
+ left: auto;
+ }
+ .modern-landing-page.manga-style .taisho-cover.mission-mode .cover-content {
+ grid-template-columns: 1fr;
+ grid-template-areas: "masthead" "hero";
+ padding: 64px 20px 28px; /* 48 nav + 16 breathing */
+ gap: 16px;
+ }
+}
+
+/* ----------------------------------------------------------------
+ 4. Masthead — vertical kanji as a quiet column, not an arcade box
+ ---------------------------------------------------------------- */
+
+.modern-landing-page.manga-style .masthead {
+ gap: 18px;
+ padding-top: 4px;
+}
+
+.modern-landing-page.manga-style .masthead-vertical {
+ background: transparent;
+ border: 0;
+ border-left: 1px solid var(--hz-line-strong);
+ box-shadow: none;
+ color: var(--hz-ink);
+ padding: 6px 0 6px 18px;
+ letter-spacing: 0.5em;
+ font-family: 'Noto Serif JP', 'Hiragino Mincho Pro', serif;
+ font-weight: 400;
+}
+.modern-landing-page.manga-style .masthead-vertical::after { display: none; }
+.modern-landing-page.manga-style .masthead-vertical .jp {
+ font-size: 32px;
+ font-weight: 400;
+ text-shadow: 0 1px 12px rgba(232, 184, 122, 0.18);
+ color: var(--hz-ink);
+ letter-spacing: 0.35em;
+}
+.modern-landing-page.manga-style .masthead-vertical .en {
+ font-size: 9px;
+ margin-top: 14px;
+ letter-spacing: 0.4em;
+ font-family: 'Inter', system-ui, sans-serif;
+ font-weight: 500;
+ color: var(--hz-amber);
+ opacity: 0.78;
+}
+
+/* Mission-mode: gentle scale instead of glow swap */
+.modern-landing-page.manga-style .taisho-cover.mission-mode .masthead-vertical {
+ transform: translateX(-2px) scale(0.95);
+ box-shadow: none;
+}
+
+/* ----------------------------------------------------------------
+ 5. Issue badge — flat pill, no chunky borders
+ ---------------------------------------------------------------- */
+
+.modern-landing-page.manga-style .issue-badge {
+ background: rgba(11, 17, 36, 0.55);
+ color: var(--hz-ink);
+ border: 1px solid var(--hz-line);
+ border-radius: 999px;
+ padding: 4px 12px;
+ font-family: 'Inter', system-ui, sans-serif;
+ font-size: 10px;
+ font-weight: 500;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ box-shadow: none;
+ backdrop-filter: blur(6px);
+ -webkit-backdrop-filter: blur(6px);
+}
+.modern-landing-page.manga-style .issue-badge::before { display: none; }
+.modern-landing-page.manga-style .issue-badge .led-heart,
+.modern-landing-page.manga-style .issue-badge .led-heart::before,
+.modern-landing-page.manga-style .issue-badge .led-heart::after {
+ background: var(--hz-mag);
+ box-shadow: 0 0 6px rgba(217, 106, 138, 0.7);
+ transform: rotate(-45deg);
+ width: 6px; height: 6px;
+}
+.modern-landing-page.manga-style .issue-badge .led-heart {
+ animation: hz-heart-pulse 2.6s ease-in-out infinite;
+}
+@keyframes hz-heart-pulse {
+ 0%, 100% { opacity: 0.85; }
+ 50% { opacity: 1; filter: drop-shadow(0 0 4px rgba(217, 106, 138, 0.9)); }
+}
+
+/* ----------------------------------------------------------------
+ 6. Hero — empty default state gets a subtle tactical reticle
+ ---------------------------------------------------------------- */
+
+.modern-landing-page.manga-style .hero {
+ position: relative;
+ min-height: 320px;
+}
+.modern-landing-page.manga-style .hero::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background:
+ /* corner brackets */
+ linear-gradient(var(--hz-line-strong), var(--hz-line-strong)) 0 0/24px 1px no-repeat,
+ linear-gradient(var(--hz-line-strong), var(--hz-line-strong)) 0 0/1px 24px no-repeat,
+ linear-gradient(var(--hz-line-strong), var(--hz-line-strong)) 100% 0/24px 1px no-repeat,
+ linear-gradient(var(--hz-line-strong), var(--hz-line-strong)) 100% 0/1px 24px no-repeat,
+ linear-gradient(var(--hz-line-strong), var(--hz-line-strong)) 0 100%/24px 1px no-repeat,
+ linear-gradient(var(--hz-line-strong), var(--hz-line-strong)) 0 100%/1px 24px no-repeat,
+ linear-gradient(var(--hz-line-strong), var(--hz-line-strong)) 100% 100%/24px 1px no-repeat,
+ linear-gradient(var(--hz-line-strong), var(--hz-line-strong)) 100% 100%/1px 24px no-repeat;
+ opacity: 0.65;
+}
+.modern-landing-page.manga-style .hero::after {
+ /* Replaced by the React TypewriterRotator component */
+ display: none;
+}
+
+/* Typewriter tagline — sits where the old static ::after lived */
+.hero-tagline {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ font-family: 'Noto Sans JP', 'Inter', sans-serif;
+ font-size: 12px;
+ font-weight: 400;
+ letter-spacing: 0.3em;
+ color: var(--hz-amber);
+ opacity: 0.92;
+ white-space: nowrap;
+ text-shadow: 0 0 12px rgba(232, 184, 122, 0.35);
+ text-align: center;
+ pointer-events: none;
+ max-width: 92%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ user-select: none;
+}
+.hero-tagline .tw-text { display: inline; }
+.hero-tagline .tw-caret {
+ display: inline-block;
+ width: 8px;
+ height: 1.05em;
+ margin-left: 4px;
+ vertical-align: -0.18em;
+ background: var(--hz-amber);
+ box-shadow: 0 0 8px rgba(232, 184, 122, 0.45);
+ animation: hz-caret-blink 1s steps(1) infinite;
+}
+@keyframes hz-caret-blink {
+ 0%, 50% { opacity: 1; }
+ 51%, 100% { opacity: 0; }
+}
+@media (max-width: 768px) {
+ .hero-tagline {
+ font-size: 10px;
+ letter-spacing: 0.22em;
+ white-space: normal; /* allow wrap on narrow phones */
+ line-height: 1.6;
+ max-width: 86%;
+ }
+ .hero-tagline .tw-caret { width: 6px; }
+}
+@media (prefers-reduced-motion: reduce) {
+ .hero-tagline .tw-caret { animation: none; opacity: 1; }
+}
+
+/* ----------------------------------------------------------------
+ 7. Mission / Makima screens — Heisei magazine column
+ ---------------------------------------------------------------- */
+
+/* No more wrapper-level accent rule — body text sits flat without a
+ coloured indent line (the SVG owns the only visible frame on the page) */
+.mission-screen {
+ gap: 20px;
+ padding-left: 0;
+ border-left: 0;
+ max-width: 720px;
+}
+.mission-screen.makima-screen {
+ gap: 14px;
+}
+
+.mission-headline {
+ font-family: 'Noto Serif JP', 'Hiragino Mincho Pro', Georgia, serif;
+ font-weight: 400;
+ font-size: 30px;
+ line-height: 1.4;
+ color: var(--hz-ink);
+ letter-spacing: 0.02em;
+ text-shadow: 0 1px 16px rgba(11, 17, 36, 0.6);
+ white-space: normal;
+ text-overflow: clip;
+ overflow: visible;
+ margin: 0;
+ padding-left: 0; /* rule + indent now live on .mission-screen */
+ border-left: 0;
+}
+
+.mission-image {
+ border: 0;
+ border-radius: 0; /* no plate corners — image floats on backdrop */
+ /* SVG already draws its own hairline frame + corner brackets at the
+ viewBox edge — drop the ring outline so we don't double-frame. */
+ box-shadow: none; /* no outward halo around bounding box */
+ filter: none; /* tactical SVG renders flat */
+ height: clamp(180px, 36vh, 360px);
+ width: 100%;
+ display: block;
+ object-fit: contain; /* show full reticle, no crop */
+ background: transparent; /* no dark plate behind letterboxed SVG */
+ animation: none; /* kill pc98 mission-pan */
+}
+
+/* ----- Desktop (≥1025px): image on the left, copy on the right ----- */
+@media (min-width: 1025px) {
+ .mission-screen {
+ /* Override pc98's grid; use flex-column for the right-column text flow.
+ Image is absolutely positioned in the reserved left gutter. */
+ display: flex !important;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 0;
+ position: relative;
+ max-width: 980px;
+ padding-left: calc(360px + 32px) !important;
+ border-left: 0 !important;
+ min-height: clamp(220px, 44vh, 420px);
+ }
+ .mission-screen .mission-image {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 360px;
+ height: clamp(220px, 44vh, 420px);
+ object-fit: cover;
+ /* Pc98 sets `grid-area: hero` etc — clear leftover row hints */
+ grid-area: auto;
+ /* Tactical SVG: render whole frame, no pan/crop, no filter */
+ animation: none !important;
+ filter: none !important;
+ background: transparent; /* no dark rounded plate behind SVG */
+ object-fit: contain;
+ object-position: center;
+ padding: 0;
+ border-radius: 0; /* no rounded plate corners */
+ /* No box-shadow — outward halo on bounding box was bleeding past the SVG */
+ box-shadow: none;
+ }
+ /* Right-column text: no accent rule — keep the layout clean.
+ The SVG image on the left supplies all the framing. */
+ .mission-screen .mission-headline,
+ .mission-screen .mission-paragraph {
+ padding-left: 0;
+ border-left: 0;
+ margin: 0;
+ }
+ .mission-screen .mission-headline + .mission-paragraph {
+ margin-top: 14px;
+ }
+ .mission-screen .mission-paragraph + .mission-paragraph {
+ margin-top: 8px;
+ }
+ /* Badge: align with the rest of the right-column flow (no indent). */
+ .mission-screen.makima-screen .makima-badge {
+ align-self: flex-start;
+ margin: 0 0 12px 0;
+ order: -1;
+ }
+ .mission-screen.makima-screen .mission-headline { order: 0; }
+ .mission-screen.makima-screen .mission-paragraph { order: 1; }
+}
+
+.mission-paragraph {
+ font-family: 'Inter', 'Noto Sans JP', system-ui, sans-serif;
+ font-size: 14px;
+ font-weight: 300;
+ line-height: 1.75;
+ color: rgba(240, 234, 223, 0.86);
+ letter-spacing: 0.01em;
+ margin: 0;
+ max-width: 60ch;
+}
+.mission-paragraph + .mission-paragraph { margin-top: 4px; }
+
+/* Makima panel — tactical accent */
+.makima-headline {
+ color: var(--hz-ink);
+ letter-spacing: 0.04em;
+}
+.makima-badge {
+ background: rgba(217, 106, 138, 0.10);
+ border: 1px solid rgba(217, 106, 138, 0.45);
+ color: var(--hz-mag);
+ font-family: 'Inter', system-ui, sans-serif;
+ font-weight: 500;
+ font-size: 10px;
+ letter-spacing: 0.22em;
+ border-radius: 2px;
+ align-self: start;
+ justify-self: start; /* don't stretch across the grid column */
+ width: max-content;
+ padding: 4px 10px;
+}
+.makima-logo {
+ border: 0;
+ border-radius: 0;
+ background: transparent; /* no dark plate behind small SVG icon */
+ box-shadow: none; /* no outward halo around bounding box */
+}
+
+/* ----------------------------------------------------------------
+ 8. CTA navbar — pinned below the floating header (desktop + mobile)
+ ---------------------------------------------------------------- */
+
+/* Promote .cta-area from grid-bottom to a fixed top navbar.
+ Overrides pc98.css (`position: absolute; bottom: 0`) and
+ mobile.css (`position: fixed; bottom: 0`). */
+.cta-area {
+ position: fixed !important;
+ top: 72px; /* sits flush under .floating-header */
+ left: 0 !important;
+ right: 0 !important;
+ bottom: auto !important;
+ z-index: 90; /* below header (100) but above content */
+ display: flex;
+ flex-direction: row !important;
+ justify-content: space-between;
+ align-items: center;
+ gap: 16px;
+ padding: 8px 32px !important;
+ height: 52px;
+ background: linear-gradient(180deg, rgba(11, 17, 36, 0.78), rgba(11, 17, 36, 0.45));
+ backdrop-filter: blur(12px) saturate(140%);
+ -webkit-backdrop-filter: blur(12px) saturate(140%);
+ border-top: 0;
+ border-bottom: 1px solid var(--hz-line);
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04);
+}
+
+.cta-left {
+ display: flex !important;
+ flex-direction: row !important;
+ align-items: center;
+ gap: 4px;
+}
+
+.cta-right {
+ display: flex;
+ align-items: center;
+}
+
+/* Buttons themselves: ghost links inside the bar, not chunky boxes */
+.taisho-cta {
+ background: transparent;
+ color: var(--hz-ink);
+ border: 0 !important;
+ outline: 0;
+ padding: 8px 16px;
+ margin: 0 !important; /* nuke pc98.css mobile-block margin-top */
+ font-family: 'Inter', system-ui, sans-serif;
+ font-weight: 500;
+ font-size: 11px;
+ letter-spacing: 0.22em;
+ text-transform: uppercase;
+ box-shadow: none !important;
+ transform: none !important;
+ border-radius: 2px;
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ transition: color 180ms ease, background 180ms ease;
+ cursor: pointer;
+ position: relative;
+ overflow: hidden;
+ opacity: 0.82;
+}
+.taisho-cta::before {
+ content: '';
+ position: absolute;
+ left: 50%;
+ right: 50%;
+ bottom: 4px;
+ height: 1px;
+ background: var(--hz-amber);
+ transition: left 200ms ease, right 200ms ease;
+}
+.taisho-cta:hover {
+ background: transparent;
+ color: var(--hz-amber);
+ opacity: 1;
+ transform: none !important;
+ box-shadow: none !important;
+}
+.taisho-cta:hover::before { left: 16px; right: 16px; }
+
+/* Active panel button — persistent highlight (replaces "Close" label) */
+.cta-left .taisho-cta.is-active,
+.cta-left .taisho-cta[aria-current="page"] {
+ color: var(--hz-amber);
+ opacity: 1;
+}
+.cta-left .taisho-cta.is-active::before,
+.cta-left .taisho-cta[aria-current="page"]::before {
+ left: 16px;
+ right: 16px;
+ height: 2px;
+ background: var(--hz-amber);
+ box-shadow: 0 0 12px rgba(232, 184, 122, 0.35);
+}
+
+/* Login keeps a primary-action treatment, but compact for the bar.
+ The hover effect avoids the amber drop-shadow halo (which read as
+ the same glow language as the active mission/makima nav buttons)
+ and uses an "inverted ink + chevron rule" treatment instead so it
+ stays clearly the primary CTA without overlapping signal. */
+.cta-right .taisho-cta {
+ background: linear-gradient(180deg, rgba(232, 184, 122, 0.95) 0%, rgba(217, 106, 138, 0.85) 100%);
+ border: 0 !important;
+ color: #1a1226;
+ font-weight: 600;
+ padding: 8px 22px;
+ opacity: 1;
+ letter-spacing: 0.18em;
+ border-radius: 2px;
+ /* Local positioning context for the chevron rule pseudo-element */
+ position: relative;
+ isolation: isolate;
+ transition:
+ background 200ms ease,
+ color 200ms ease,
+ letter-spacing 240ms ease;
+}
+.cta-right .taisho-cta::before { display: none; }
+
+/* Top-edge tactical chevron rule: hidden by default, slides in on hover.
+ Uses ::after so it's independent of the existing ::before underline rule. */
+.cta-right .taisho-cta::after {
+ content: '';
+ position: absolute;
+ inset: 0 0 auto 0;
+ height: 2px;
+ background: var(--hz-ink);
+ transform: scaleX(0);
+ transform-origin: left center;
+ transition: transform 220ms ease-out;
+ pointer-events: none;
+}
+
+.cta-right .taisho-cta:hover {
+ /* Invert: dark plate, light ink — opposite of the resting state.
+ Reads as "armed / pressed" instead of "glowing". */
+ background: #0b1124;
+ color: var(--hz-amber);
+ letter-spacing: 0.22em; /* subtle ink-spread */
+ /* Explicitly cancel any inherited glow */
+ box-shadow: none !important;
+ filter: none;
+}
+.cta-right .taisho-cta:hover::after {
+ transform: scaleX(1);
+}
+.cta-right .taisho-cta:active {
+ background: #1a2548;
+ color: var(--hz-ink);
+ letter-spacing: 0.20em;
+}
+.cta-right .taisho-cta:focus-visible {
+ outline: 1px solid var(--hz-amber);
+ outline-offset: 2px;
+}
+
+.cta-icon { font-size: 10px; opacity: 0.85; margin-right: 2px; }
+.cta-text { font-size: 11px; }
+
+/* ----------------------------------------------------------------
+ 9. Bottom telemetry strip — GitS:SAC HUD pill
+ ---------------------------------------------------------------- */
+
+.bottom-stats { bottom: 24px; }
+.bottom-stats .rf-stats {
+ background: rgba(11, 17, 36, 0.55);
+ border: 1px solid var(--hz-line);
+ border-radius: 2px;
+ padding: 8px 18px;
+ display: flex;
+ gap: 28px;
+ font-family: 'Inter', system-ui, sans-serif;
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ position: relative;
+}
+.bottom-stats .rf-stats::before,
+.bottom-stats .rf-stats::after {
+ content: '';
+ position: absolute;
+ width: 8px;
+ height: 8px;
+ border: 1px solid var(--hz-amber);
+ opacity: 0.7;
+}
+.bottom-stats .rf-stats::before { top: -1px; left: -1px; border-right: 0; border-bottom: 0; }
+.bottom-stats .rf-stats::after { bottom: -1px; right: -1px; border-left: 0; border-top: 0; }
+
+.bottom-stats .rf-stat {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ font-size: 11px;
+ letter-spacing: 0.1em;
+}
+.bottom-stats .rf-stat .label {
+ color: var(--hz-cyan);
+ font-size: 9px;
+ letter-spacing: 0.22em;
+ text-transform: uppercase;
+ font-weight: 500;
+ opacity: 0.85;
+}
+.bottom-stats .rf-stat .value {
+ color: var(--hz-ink);
+ font-family: 'Inter', 'JetBrains Mono', monospace;
+ font-feature-settings: 'tnum';
+ font-variant-numeric: tabular-nums;
+ font-weight: 500;
+ font-size: 12px;
+}
+
+/* ----------------------------------------------------------------
+ 10. Mobile refinements
+ ---------------------------------------------------------------- */
+
+@media (max-width: 768px) {
+ .floating-header { height: 56px; padding: 0 16px; }
+ .floating-header .brand { left: 16px; }
+ .floating-header .brand-mark { height: 22px !important; }
+ .system-info { right: 16px; gap: 12px; }
+ .info-label { font-size: 9px; }
+ .info-value { font-size: 10px; }
+
+ /* Mobile navbar — sits below the shorter mobile header */
+ .cta-area {
+ top: 56px;
+ height: 48px;
+ padding: 0 14px !important; /* center via flex, not via padding */
+ gap: 8px;
+ align-items: center !important;
+ }
+ .cta-left,
+ .cta-right {
+ height: auto !important; /* don't inherit any column-layout height */
+ align-self: center !important;
+ align-items: center !important;
+ }
+ .cta-left { gap: 0; flex-direction: row !important; }
+ .taisho-cta {
+ padding: 0 10px; /* let height drive vertical centering */
+ height: 32px;
+ line-height: 32px;
+ font-size: 10px;
+ letter-spacing: 0.18em;
+ align-self: center !important;
+ margin: 0 !important; /* kill pc98.css mobile margin-top: 20px */
+ }
+ .cta-right .taisho-cta { padding: 0 14px; font-size: 10px; height: 32px; line-height: 32px; }
+ .cta-text { font-size: 10px; line-height: 32px; }
+ .cta-icon { font-size: 9px; line-height: 32px; }
+
+ .modern-landing-page.manga-style .taisho-cover .cover-content {
+ padding: 124px 20px 28px;
+ gap: 22px;
+ }
+
+ .modern-landing-page.manga-style .masthead-vertical .jp { font-size: 24px; }
+ .modern-landing-page.manga-style .masthead-vertical .en { font-size: 8px; }
+
+ .mission-headline { font-size: 22px; padding-left: 0; }
+ .mission-paragraph { font-size: 13px; }
+
+ .modern-landing-page.manga-style .hero::after { font-size: 9px; letter-spacing: 0.2em; }
+
+ .bottom-stats { bottom: calc(20px + env(safe-area-inset-bottom, 0px)); }
+}
+
+/* ----------------------------------------------------------------
+ 11. Reduced-motion
+ ---------------------------------------------------------------- */
+
+@media (prefers-reduced-motion: reduce) {
+ .modern-landing-page.manga-style .issue-badge .led-heart { animation: none; }
+ .mission-image { animation: none; }
+}
diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo
deleted file mode 100644
index 79408dc..0000000
--- a/frontend/tsconfig.tsbuildinfo
+++ /dev/null
@@ -1 +0,0 @@
-{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/bottombar.tsx","./src/components/choicemenu.tsx","./src/components/cityscapebackground.tsx","./src/components/configmodal.tsx","./src/components/contractdetail.tsx","./src/components/contractlist.tsx","./src/components/dialoguebox.tsx","./src/components/filedetail.tsx","./src/components/heartlogo.tsx","./src/components/landingpage.tsx","./src/components/loadingscreen.tsx","./src/components/origamidragonlogo.tsx","./src/components/topbar.tsx","./src/components/vnapp.tsx","./src/components/vninterface.tsx","./src/components/vnviewport.tsx","./src/services/ws.ts","./src/stores/index.ts"],"version":"5.9.2"} \ No newline at end of file