summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu-co <bot@soryu.co>2026-05-06 12:39:02 +0000
committersoryu-co <bot@soryu.co>2026-05-06 12:39:02 +0000
commit30bfe2149fdead196c08a8cfaf5bb5415e6ee7c4 (patch)
tree393934a1aadcca9fb42a517bad84faf679df2229
parent771ece20e5e669f93fc0aba0459ac1ff2fa0825b (diff)
downloadsoryu-30bfe2149fdead196c08a8cfaf5bb5415e6ee7c4.tar.gz
soryu-30bfe2149fdead196c08a8cfaf5bb5415e6ee7c4.zip
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.
-rw-r--r--docs/heisei-screenshots/after-01-desktop-default.jpgbin105132 -> 106576 bytes
-rw-r--r--docs/heisei-screenshots/after-02-desktop-mission.jpgbin299749 -> 230347 bytes
-rw-r--r--docs/heisei-screenshots/after-03-desktop-makima.jpgbin97563 -> 96847 bytes
-rw-r--r--docs/heisei-screenshots/after-04-desktop-login-hover.jpgbin105240 -> 105994 bytes
-rw-r--r--docs/heisei-screenshots/after-05-mobile-default.jpgbin58794 -> 64257 bytes
-rw-r--r--docs/heisei-screenshots/after-06-mobile-mission.jpgbin177836 -> 181240 bytes
-rw-r--r--frontend/src/components/LandingPage.tsx15
-rw-r--r--frontend/src/components/TypewriterRotator.tsx84
-rw-r--r--frontend/src/styles/heisei.css89
9 files changed, 178 insertions, 10 deletions
diff --git a/docs/heisei-screenshots/after-01-desktop-default.jpg b/docs/heisei-screenshots/after-01-desktop-default.jpg
index 4a35ffb..c7e8cc7 100644
--- a/docs/heisei-screenshots/after-01-desktop-default.jpg
+++ b/docs/heisei-screenshots/after-01-desktop-default.jpg
Binary files differ
diff --git a/docs/heisei-screenshots/after-02-desktop-mission.jpg b/docs/heisei-screenshots/after-02-desktop-mission.jpg
index 6163d0e..6e5bb6a 100644
--- a/docs/heisei-screenshots/after-02-desktop-mission.jpg
+++ b/docs/heisei-screenshots/after-02-desktop-mission.jpg
Binary files differ
diff --git a/docs/heisei-screenshots/after-03-desktop-makima.jpg b/docs/heisei-screenshots/after-03-desktop-makima.jpg
index c66521e..d9de764 100644
--- a/docs/heisei-screenshots/after-03-desktop-makima.jpg
+++ b/docs/heisei-screenshots/after-03-desktop-makima.jpg
Binary files differ
diff --git a/docs/heisei-screenshots/after-04-desktop-login-hover.jpg b/docs/heisei-screenshots/after-04-desktop-login-hover.jpg
index 3c4b86f..523f7cd 100644
--- a/docs/heisei-screenshots/after-04-desktop-login-hover.jpg
+++ b/docs/heisei-screenshots/after-04-desktop-login-hover.jpg
Binary files differ
diff --git a/docs/heisei-screenshots/after-05-mobile-default.jpg b/docs/heisei-screenshots/after-05-mobile-default.jpg
index 0ac8b14..ddefbb1 100644
--- a/docs/heisei-screenshots/after-05-mobile-default.jpg
+++ b/docs/heisei-screenshots/after-05-mobile-default.jpg
Binary files differ
diff --git a/docs/heisei-screenshots/after-06-mobile-mission.jpg b/docs/heisei-screenshots/after-06-mobile-mission.jpg
index 618c0c7..6f35278 100644
--- a/docs/heisei-screenshots/after-06-mobile-mission.jpg
+++ b/docs/heisei-screenshots/after-06-mobile-mission.jpg
Binary files differ
diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx
index 931fe68..708cbb1 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
@@ -189,7 +198,11 @@ 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 */}
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/styles/heisei.css b/frontend/src/styles/heisei.css
index 356080e..4017f83 100644
--- a/frontend/src/styles/heisei.css
+++ b/frontend/src/styles/heisei.css
@@ -37,13 +37,29 @@ 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;
- height: calc(100vh - 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(100vh - 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;
}
}
@@ -303,27 +319,75 @@ body { background: var(--hz-night); color: var(--hz-ink); }
opacity: 0.65;
}
.modern-landing-page.manga-style .hero::after {
- content: '低遅延ストリーミング · LOW-LATENCY OBSERVABILITY';
+ /* 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: 11px;
+ font-size: 12px;
font-weight: 400;
letter-spacing: 0.3em;
color: var(--hz-amber);
- opacity: 0.75;
+ 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
---------------------------------------------------------------- */
+/* Move the accent rule + indent onto the wrapper so headline, image,
+ and body share one consistent column edge */
.mission-screen {
gap: 20px;
+ padding-left: 18px;
+ border-left: 2px solid var(--hz-amber);
+ max-width: 720px;
+}
+.mission-screen.makima-screen {
+ border-left-color: var(--hz-mag);
+ gap: 14px;
}
.mission-headline {
@@ -337,9 +401,9 @@ body { background: var(--hz-night); color: var(--hz-ink); }
white-space: normal;
text-overflow: clip;
overflow: visible;
- max-width: 56ch;
- border-left: 2px solid var(--hz-amber);
- padding-left: 18px;
+ margin: 0;
+ padding-left: 0; /* rule + indent now live on .mission-screen */
+ border-left: 0;
}
.mission-image {
@@ -350,6 +414,8 @@ body { background: var(--hz-night); color: var(--hz-ink); }
0 0 0 1px var(--hz-line);
filter: saturate(0.85) brightness(0.92);
height: clamp(180px, 36vh, 360px);
+ width: 100%;
+ display: block;
}
.mission-paragraph {
@@ -359,14 +425,15 @@ body { background: var(--hz-night); color: var(--hz-ink); }
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;
- border-left-color: var(--hz-mag);
}
.makima-badge {
background: rgba(217, 106, 138, 0.10);
@@ -377,6 +444,10 @@ body { background: var(--hz-night); color: var(--hz-ink); }
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;