import { useState, useEffect, useRef } from "react"; const styles = ` @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@300;400;500&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,300;1,400&family=DM+Mono:ital,wght@0,300;0,400;1,300&display=swap'); *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } :root { --black: #080706; --black-2: #0f0e0c; --black-3: #181614; --black-4: #221f1c; --amber: #c9a84c; --amber-dim: #8b6e2e; --amber-light: #e8c96a; --cream: #f0e8da; --cream-dim: #b8ab99; --cream-muted: #6e6358; --red: #9b3a3a; --red-dim: #7a2e2e; --serif: 'Cormorant Garamond', Georgia, serif; --jp: 'Noto Serif JP', serif; --mono: 'DM Mono', monospace; } html, body { height: 100%; background: var(--black); color: var(--cream); } .app { min-height: 100vh; background: var(--black); font-family: var(--serif); overflow-x: hidden; } /* NAV */ .nav { position: fixed; top: 0; left: 0; right: 0; z-index: 100; display: flex; align-items: center; justify-content: space-between; padding: 0 2.5rem; height: 56px; background: rgba(8,7,6,0.92); backdrop-filter: blur(12px); border-bottom: 1px solid rgba(201,168,76,0.12); } .nav-logo { font-family: var(--jp); font-size: 1.1rem; color: var(--amber); letter-spacing: 0.08em; cursor: pointer; transition: opacity 0.2s; } .nav-logo:hover { opacity: 0.7; } .nav-right { display: flex; align-items: center; gap: 2rem; } .nav-link { font-family: var(--mono); font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--cream-muted); cursor: pointer; transition: color 0.2s; background: none; border: none; } .nav-link:hover { color: var(--cream); } .nav-link.active { color: var(--amber); } /* SCREEN WRAPPER */ .screen { min-height: 100vh; padding-top: 56px; } /* ==================== BROWSE SCREEN ==================== */ .browse-hero { padding: 5rem 2.5rem 3rem; max-width: 960px; margin: 0 auto; } .browse-eyebrow { font-family: var(--mono); font-size: 10px; letter-spacing: 0.3em; text-transform: uppercase; color: var(--amber); margin-bottom: 1.5rem; display: flex; align-items: center; gap: 1rem; } .browse-eyebrow::after { content: ''; flex: 1; height: 1px; background: rgba(201,168,76,0.2); max-width: 80px; } .browse-title { font-family: var(--jp); font-size: clamp(2.8rem, 6vw, 4.5rem); font-weight: 300; color: var(--cream); line-height: 1.1; margin-bottom: 0.5rem; } .browse-subtitle { font-size: 1.1rem; font-style: italic; color: var(--cream-dim); font-weight: 300; margin-bottom: 0.8rem; } .browse-desc { font-size: 0.95rem; color: var(--cream-muted); line-height: 1.7; max-width: 520px; font-family: var(--mono); font-size: 11px; letter-spacing: 0.03em; } .browse-grid { max-width: 960px; margin: 0 auto; padding: 0 2.5rem 5rem; display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: rgba(201,168,76,0.08); border: 1px solid rgba(201,168,76,0.08); } .category-card { background: var(--black-2); padding: 2.5rem 2rem; cursor: pointer; position: relative; overflow: hidden; transition: background 0.3s; border: none; text-align: left; color: inherit; } .category-card::before { content: ''; position: absolute; inset: 0; background: rgba(201,168,76,0.04); opacity: 0; transition: opacity 0.3s; } .category-card:hover { background: var(--black-3); } .category-card:hover::before { opacity: 1; } .category-card:hover .cat-arrow { transform: translateX(4px); opacity: 1; } .cat-num { font-family: var(--mono); font-size: 10px; color: var(--amber-dim); letter-spacing: 0.2em; margin-bottom: 1.2rem; } .cat-jp { font-family: var(--jp); font-size: 1.6rem; color: var(--cream); margin-bottom: 0.3rem; font-weight: 400; } .cat-en { font-family: var(--mono); font-size: 10px; letter-spacing: 0.15em; text-transform: uppercase; color: var(--cream-muted); margin-bottom: 1rem; } .cat-desc { font-size: 0.82rem; color: var(--cream-muted); line-height: 1.6; font-style: italic; } .cat-arrow { position: absolute; bottom: 1.5rem; right: 1.5rem; color: var(--amber); font-size: 1rem; opacity: 0; transition: all 0.3s; } .cat-count { position: absolute; top: 1.2rem; right: 1.5rem; font-family: var(--mono); font-size: 9px; color: var(--cream-muted); letter-spacing: 0.1em; } .schema-list { max-width: 960px; margin: 0 auto; padding: 0 2.5rem 3rem; } .schema-list-header { font-family: var(--mono); font-size: 10px; letter-spacing: 0.25em; text-transform: uppercase; color: var(--cream-muted); padding-bottom: 1rem; border-bottom: 1px solid rgba(255,255,255,0.06); margin-bottom: 0; } .schema-row { display: flex; align-items: center; justify-content: space-between; padding: 1.2rem 0; border-bottom: 1px solid rgba(255,255,255,0.04); cursor: pointer; transition: all 0.2s; gap: 1rem; } .schema-row:hover { padding-left: 0.5rem; } .schema-row:hover .schema-row-jp { color: var(--amber); } .schema-row-left { display: flex; align-items: center; gap: 1.5rem; } .schema-row-jp { font-family: var(--jp); font-size: 1.2rem; color: var(--cream); transition: color 0.2s; white-space: nowrap; } .schema-row-info { display: flex; flex-direction: column; gap: 0.2rem; } .schema-row-reading { font-family: var(--mono); font-size: 10px; color: var(--cream-muted); letter-spacing: 0.1em; } .schema-row-gloss { font-size: 0.85rem; color: var(--cream-dim); font-style: italic; } .schema-row-cat { font-family: var(--mono); font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--amber-dim); background: rgba(201,168,76,0.08); padding: 0.25rem 0.6rem; white-space: nowrap; } /* ==================== WORLD ENTRY SCREEN ==================== */ .world-entry { min-height: 100vh; background: var(--black); display: flex; flex-direction: column; position: relative; overflow: hidden; } .we-back { position: absolute; top: 72px; left: 2rem; font-family: var(--mono); font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--cream-muted); cursor: pointer; background: none; border: none; color: inherit; transition: color 0.2s; z-index: 10; } .we-back:hover { color: var(--amber); } .we-image-layer { position: absolute; inset: 0; z-index: 0; } .we-image-layer img { width: 100%; height: 100%; object-fit: cover; opacity: 0.18; filter: grayscale(60%); transition: opacity 3s ease; } .we-image-layer.loaded img { opacity: 0.22; } .we-gradient { position: absolute; inset: 0; background: linear-gradient( to bottom, rgba(8,7,6,0.5) 0%, rgba(8,7,6,0.2) 30%, rgba(8,7,6,0.7) 70%, rgba(8,7,6,0.98) 100% ); } .we-content { position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 6rem 2rem 4rem; text-align: center; } .we-phase-label { font-family: var(--mono); font-size: 9px; letter-spacing: 0.35em; text-transform: uppercase; color: var(--amber); margin-bottom: 3rem; opacity: 0; transition: opacity 1s ease; } .we-phase-label.visible { opacity: 1; } .we-text-line { font-size: clamp(1.1rem, 2.5vw, 1.5rem); font-style: italic; font-weight: 300; color: var(--cream); line-height: 1.6; margin-bottom: 1.5rem; opacity: 0; transform: translateY(8px); transition: opacity 1.2s ease, transform 1.2s ease; max-width: 540px; } .we-text-line.visible { opacity: 1; transform: translateY(0); } .we-jp-title { font-family: var(--jp); font-size: clamp(3rem, 7vw, 5.5rem); font-weight: 300; color: var(--cream); letter-spacing: 0.1em; margin: 2rem 0 0.5rem; opacity: 0; transition: opacity 1.5s ease; } .we-jp-title.visible { opacity: 1; } .we-reading { font-family: var(--mono); font-size: 12px; letter-spacing: 0.2em; color: var(--amber); margin-bottom: 0.4rem; opacity: 0; transition: opacity 1s ease 0.3s; } .we-reading.visible { opacity: 1; } .we-gloss { font-size: 1rem; font-style: italic; color: var(--cream-dim); margin-bottom: 2.5rem; opacity: 0; transition: opacity 1s ease 0.5s; } .we-gloss.visible { opacity: 1; } .we-logic { font-size: 0.9rem; color: var(--cream-muted); max-width: 480px; line-height: 1.75; border-top: 1px solid rgba(201,168,76,0.2); padding-top: 1.5rem; margin-top: 0.5rem; opacity: 0; transition: opacity 1s ease 0.8s; } .we-logic.visible { opacity: 1; } .we-continue-btn { margin-top: 3rem; padding: 0.9rem 3rem; background: transparent; border: 1px solid rgba(201,168,76,0.4); color: var(--amber); font-family: var(--mono); font-size: 10px; letter-spacing: 0.25em; text-transform: uppercase; cursor: pointer; transition: all 0.3s; opacity: 0; transition: opacity 1s ease 1.2s, background 0.3s, border-color 0.3s; } .we-continue-btn.visible { opacity: 1; } .we-continue-btn:hover { background: rgba(201,168,76,0.1); border-color: var(--amber); } .we-cluster { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; margin-top: 1.5rem; opacity: 0; transition: opacity 1s ease 1.5s; } .we-cluster.visible { opacity: 1; } .cluster-tag { font-family: var(--jp); font-size: 0.8rem; color: var(--cream-muted); background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.06); padding: 0.3rem 0.8rem; cursor: pointer; transition: all 0.2s; } .cluster-tag:hover { color: var(--amber); border-color: rgba(201,168,76,0.3); } /* ==================== LANGUAGE OUTPUT SCREEN ==================== */ .lang-screen { min-height: 100vh; background: var(--black-2); } .lang-header { padding: 3rem 2.5rem 2rem; max-width: 800px; margin: 0 auto; border-bottom: 1px solid rgba(255,255,255,0.05); } .lang-breadcrumb { font-family: var(--mono); font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--cream-muted); margin-bottom: 1.5rem; display: flex; align-items: center; gap: 0.5rem; cursor: pointer; } .lang-breadcrumb:hover { color: var(--amber); } .lang-title-jp { font-family: var(--jp); font-size: 2.5rem; font-weight: 300; color: var(--cream); margin-bottom: 0.3rem; } .lang-title-en { font-size: 1rem; font-style: italic; color: var(--cream-dim); margin-bottom: 0.8rem; } .lang-social-logic { font-family: var(--mono); font-size: 11px; color: var(--cream-muted); line-height: 1.7; letter-spacing: 0.03em; padding: 1rem 1.2rem; border-left: 2px solid var(--amber-dim); background: rgba(201,168,76,0.04); margin-top: 1rem; } .lang-section-label { font-family: var(--mono); font-size: 9px; letter-spacing: 0.3em; text-transform: uppercase; color: var(--amber); padding: 2rem 2.5rem 1rem; max-width: 800px; margin: 0 auto; } .vocab-list { max-width: 800px; margin: 0 auto; padding: 0 2.5rem; } .vocab-item { border-bottom: 1px solid rgba(255,255,255,0.04); padding: 1.5rem 0; cursor: pointer; transition: all 0.2s; } .vocab-item:hover { padding-left: 0.4rem; } .vocab-item:hover .vi-jp { color: var(--amber-light); } .vi-top { display: flex; align-items: baseline; gap: 1rem; margin-bottom: 0.4rem; flex-wrap: wrap; } .vi-jp { font-family: var(--jp); font-size: 1.4rem; color: var(--cream); transition: color 0.2s; } .vi-reading { font-family: var(--mono); font-size: 11px; color: var(--cream-muted); letter-spacing: 0.08em; } .vi-en { font-size: 0.9rem; font-style: italic; color: var(--cream-dim); margin-left: auto; } .vi-note { font-size: 0.83rem; color: var(--cream-muted); line-height: 1.65; max-width: 560px; } .vi-note.expanded { margin-top: 0.6rem; padding: 0.8rem 1rem; background: rgba(255,255,255,0.02); border-left: 1px solid rgba(201,168,76,0.2); } .vi-num { font-family: var(--mono); font-size: 9px; color: var(--cream-muted); opacity: 0.4; margin-right: auto; width: 20px; flex-shrink: 0; } .lang-nav-buttons { max-width: 800px; margin: 3rem auto 0; padding: 2rem 2.5rem 4rem; display: flex; gap: 1rem; } .btn-primary { padding: 1rem 2.5rem; background: var(--amber); color: var(--black); font-family: var(--mono); font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; border: none; cursor: pointer; transition: all 0.2s; flex: 1; text-align: center; } .btn-primary:hover { background: var(--amber-light); } .btn-secondary { padding: 1rem 2rem; background: transparent; color: var(--cream-muted); font-family: var(--mono); font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; border: 1px solid rgba(255,255,255,0.1); cursor: pointer; transition: all 0.2s; } .btn-secondary:hover { border-color: rgba(255,255,255,0.3); color: var(--cream); } /* ==================== PRACTICE SCREEN ==================== */ .practice-screen { min-height: 100vh; background: var(--black-3); } .practice-header { padding: 3rem 2.5rem 0; max-width: 800px; margin: 0 auto; } .practice-tabs { display: flex; border-bottom: 1px solid rgba(255,255,255,0.06); margin-bottom: 0; max-width: 800px; margin: 0 auto; padding: 0 2.5rem; } .tab-btn { font-family: var(--mono); font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; padding: 1rem 1.5rem; background: none; border: none; color: var(--cream-muted); cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s; margin-bottom: -1px; } .tab-btn:hover { color: var(--cream); } .tab-btn.active { color: var(--amber); border-bottom-color: var(--amber); } .tab-content { max-width: 800px; margin: 0 auto; padding: 2.5rem; } /* Word Practice Tab */ .anki-card { background: var(--black-4); border: 1px solid rgba(255,255,255,0.06); padding: 3rem 2rem; text-align: center; margin-bottom: 1.5rem; position: relative; min-height: 220px; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; transition: all 0.3s; } .anki-card:hover { border-color: rgba(201,168,76,0.2); } .anki-schema-bg { position: absolute; inset: 0; background: radial-gradient(ellipse at center, rgba(201,168,76,0.03) 0%, transparent 70%); pointer-events: none; } .anki-jp { font-family: var(--jp); font-size: 2.5rem; font-weight: 300; color: var(--cream); margin-bottom: 0.5rem; position: relative; } .anki-front-hint { font-family: var(--mono); font-size: 9px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--cream-muted); position: absolute; bottom: 1rem; left: 50%; transform: translateX(-50%); white-space: nowrap; } .anki-back { display: flex; flex-direction: column; gap: 0.5rem; align-items: center; } .anki-reading { font-family: var(--mono); font-size: 12px; letter-spacing: 0.1em; color: var(--amber); } .anki-en { font-size: 1.1rem; font-style: italic; color: var(--cream-dim); } .anki-social { font-size: 0.8rem; color: var(--cream-muted); max-width: 420px; line-height: 1.6; margin-top: 0.5rem; padding: 0.8rem; border-top: 1px solid rgba(255,255,255,0.04); text-align: center; } .anki-controls { display: flex; gap: 1rem; justify-content: center; margin-bottom: 2rem; } .anki-btn { padding: 0.7rem 2rem; font-family: var(--mono); font-size: 10px; letter-spacing: 0.15em; text-transform: uppercase; cursor: pointer; border: 1px solid; background: none; transition: all 0.2s; } .anki-btn.again { border-color: rgba(155,58,58,0.4); color: #b05555; } .anki-btn.again:hover { background: rgba(155,58,58,0.1); } .anki-btn.good { border-color: rgba(201,168,76,0.4); color: var(--amber); } .anki-btn.good:hover { background: rgba(201,168,76,0.1); } .anki-btn.easy { border-color: rgba(76,140,90,0.4); color: #5a9e6a; } .anki-btn.easy:hover { background: rgba(76,140,90,0.1); } .anki-progress { display: flex; gap: 4px; justify-content: center; margin-bottom: 1.5rem; } .anki-dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,0.1); transition: background 0.3s; } .anki-dot.done { background: var(--amber); } .anki-dot.current { background: var(--cream); } /* Task Tab */ .task-intro { font-size: 0.9rem; color: var(--cream-dim); line-height: 1.7; margin-bottom: 2rem; font-style: italic; } .task-scenario { background: var(--black-4); border: 1px solid rgba(255,255,255,0.06); padding: 2rem; margin-bottom: 2rem; } .task-scenario-label { font-family: var(--mono); font-size: 9px; letter-spacing: 0.25em; text-transform: uppercase; color: var(--amber); margin-bottom: 1rem; } .task-exchange { display: flex; flex-direction: column; gap: 0.8rem; margin-bottom: 1.5rem; } .exchange-line { display: flex; gap: 1rem; align-items: baseline; } .exchange-speaker { font-family: var(--mono); font-size: 10px; color: var(--cream-muted); letter-spacing: 0.1em; min-width: 20px; flex-shrink: 0; } .exchange-text { font-family: var(--jp); font-size: 1rem; color: var(--cream); line-height: 1.6; } .task-question { font-size: 0.9rem; color: var(--cream-dim); line-height: 1.6; margin-bottom: 0.8rem; font-weight: 400; } .task-input { width: 100%; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); color: var(--cream); padding: 0.9rem 1rem; font-family: var(--serif); font-size: 0.9rem; line-height: 1.6; resize: vertical; min-height: 80px; transition: border-color 0.2s; margin-bottom: 1rem; } .task-input:focus { outline: none; border-color: rgba(201,168,76,0.3); } .task-input::placeholder { color: var(--cream-muted); font-style: italic; } .task-reveal { background: rgba(201,168,76,0.06); border: 1px solid rgba(201,168,76,0.15); padding: 1.5rem; margin-top: 1rem; display: none; } .task-reveal.show { display: block; } .task-reveal-label { font-family: var(--mono); font-size: 9px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--amber); margin-bottom: 0.8rem; } .task-reveal-text { font-size: 0.85rem; color: var(--cream-dim); line-height: 1.7; } /* Lesson Module Tab */ .lesson-module { background: var(--black-4); border: 1px solid rgba(255,255,255,0.06); padding: 2.5rem; margin-bottom: 1.5rem; } .lesson-meta { display: flex; gap: 2rem; margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid rgba(255,255,255,0.06); flex-wrap: wrap; } .lesson-meta-item label { font-family: var(--mono); font-size: 9px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--cream-muted); display: block; margin-bottom: 0.3rem; } .lesson-meta-item span { font-size: 0.9rem; color: var(--cream-dim); } .lesson-phase { margin-bottom: 2rem; padding-bottom: 2rem; border-bottom: 1px solid rgba(255,255,255,0.04); } .lesson-phase:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; } .phase-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; } .phase-num { font-family: var(--mono); font-size: 9px; letter-spacing: 0.1em; color: var(--amber); background: rgba(201,168,76,0.1); padding: 0.2rem 0.5rem; white-space: nowrap; } .phase-title { font-size: 0.95rem; color: var(--cream); font-weight: 400; } .phase-time { font-family: var(--mono); font-size: 9px; color: var(--cream-muted); margin-left: auto; white-space: nowrap; } .phase-body { font-size: 0.83rem; color: var(--cream-muted); line-height: 1.75; } .phase-note { margin-top: 0.8rem; padding: 0.7rem 1rem; background: rgba(255,255,255,0.02); border-left: 2px solid rgba(201,168,76,0.3); font-size: 0.8rem; color: var(--cream-muted); font-style: italic; line-height: 1.6; } /* Progress indicator */ .screen-progress { position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; z-index: 100; } .prog-step { display: flex; flex-direction: column; align-items: center; gap: 4px; cursor: pointer; opacity: 0.4; transition: opacity 0.3s; } .prog-step.active { opacity: 1; } .prog-step:hover { opacity: 0.8; } .prog-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--cream-muted); transition: background 0.3s, transform 0.3s; } .prog-step.active .prog-dot { background: var(--amber); transform: scale(1.3); } .prog-label { font-family: var(--mono); font-size: 8px; letter-spacing: 0.1em; color: var(--cream-muted); white-space: nowrap; } .prog-step.active .prog-label { color: var(--amber); } @media (max-width: 640px) { .browse-grid { grid-template-columns: 1fr; } .lang-nav-buttons { flex-direction: column; } .practice-tabs { overflow-x: auto; } .anki-controls { flex-wrap: wrap; } } `; const SCHEMAS = [ { id: "kuuki", jp: "空気を読む", reading: "kūki wo yomu", gloss: "reading the air", category: "Social Practices", categoryNum: "01", schema: `You are in a room. Something is happening that nobody is saying. The temperature of the group has shifted — not dramatically, not visibly, but it has shifted, and you can feel it if you are paying attention in the right way. The people around you know this. They have read it. They are adjusting. You are either adjusting with them, or you are the one who hasn't noticed.`, socialLogic: "In Japanese social space, the group has an atmosphere — a collective felt state — that is as real as any spoken statement. To read it is a social competence. To miss it is a social failing with consequences.", cluster: ["間 (ma)", "察する", "遠慮", "本音と建前", "阿吽の呼吸"], image: "https://images.unsplash.com/photo-1497366216548-37526070297c?w=1200&q=60", vocab: [ { jp: "空気を読む", reading: "kūki wo yomu", en: "to read the social atmosphere", note: "Used to describe the competence itself. あの人は空気が読める — that person can read the air." }, { jp: "空気が読めない", reading: "kūki ga yomenai / KY", en: "cannot read the air", note: "Not a description. A judgment. The abbreviation KY is used when the failure is visible and consequential." }, { jp: "雰囲気", reading: "fun'iki", en: "atmosphere, vibe", note: "The noun for what 空気を読む perceives. 雰囲気が変わった — the atmosphere changed." }, { jp: "察する", reading: "sassuru", en: "to sense, to infer without being told", note: "察してください — please understand without me having to say it. The social expectation of inference." }, { jp: "気を遣う", reading: "ki wo tsukau", en: "to be considerate, mindful", note: "The behavioral expression of having read the air correctly. Reading is cognitive; 気を遣う is what you do with it." }, { jp: "なんとなく", reading: "nantonaku", en: "somehow, vaguely", note: "The adverb of schema perception. なんとなく違う — something feels off, though I couldn't tell you what." }, { jp: "そうですね…", reading: "sō desu ne", en: "surface: agreement / pragmatic: possible refusal", note: "The pause after そうですね is the signal. The slow nod is the signal. Without the schema, you hear agreement. With it, you hear a door closing." }, { jp: "ちょっと難しいかもしれませんね", reading: "chotto muzukashii kamoshiremasen ne", en: "surface: it's difficult / pragmatic: refusal", note: "Fixed soft refusal. If you hear this, do not push further. The topic is closed." }, ], task: { intro: "Four text-message exchanges. In each, a social decision has been made implicitly. Identify what was decided and what linguistic signal carried it.", scenarios: [ { label: "Exchange A", lines: [ { speaker: "A", text: "今週末、みんなでバーベキューどう?" }, { speaker: "B", text: "あ、いいですね。でも、ちょっと予定があって…" }, { speaker: "A", text: "そっか、残念だね。また今度ね。" }, ], question: "What has B communicated? What was the signal? Would a learner without this schema read this correctly?", reveal: "B has declined. The signal is ちょっと予定があって… — the trailing ellipsis after a vague reference to plans is a soft refusal. A has read it correctly and closed the exchange without pressing. The exchange is already over. It closed with the ellipsis.", }, { label: "Exchange B", lines: [ { speaker: "A", text: "この企画書、どう思う?" }, { speaker: "B", text: "うーん、面白いとは思うんですけど、なんか難しいところがありそうで…" }, { speaker: "A", text: "具体的にどのあたり?" }, { speaker: "B", text: "えっと、全体的に、というか…" }, ], question: "A pressed for specifics after B's initial response. Was this a KY move? What is B doing in the final line?", reveal: "B's initial response is a soft negative. A has violated the implicit reading by pressing directly. This is a mild KY move. B cannot answer without abandoning the soft register — the second response tries to stay soft while A has moved to a direct mode. The exchange is uncomfortable for both.", }, ], }, lesson: { duration: "65 min", level: "N4–N3", size: "4–8 learners", phases: [ { num: "01", title: "Encounter — Film Clip", time: "15 min", body: "Show a 3-minute Japanese meeting scene (no subtitles). A decision is made without anyone saying no. Learners write three responses: What is happening? What has been decided? How do you know?", note: "Do not explain anything before the clip runs. The confusion is the pedagogy." }, { num: "02", title: "Unpack — Schema Named", time: "10 min", body: "Group discussion of written responses. Teacher listens and notes key words learners use. Then: introduce 空気を読む as a name for what they were just doing. Write it on the board. Say it. Let it sit. Ask: does your language have a word for this?", note: "The name must arrive after the experience, not before it." }, { num: "03", title: "Activate — Scenario Discussion", time: "20 min", body: "Four text-message exchanges (Task tab). Learners read and discuss in pairs before revealing the schema analysis. Disagreements between learners are not to be resolved — they are the data.", note: "When two learners disagree about what was communicated, name it: you are both reading the schema. You are reading different aspects of it." }, { num: "04", title: "Acquire — Language Output", time: "10 min", body: "Walk through the vocabulary network. Each item explained through social logic, not grammar. Focus on items 7 and 8 — the pragmatic expressions that look like something they are not.", note: "" }, { num: "05", title: "Produce — Exit Reflection", time: "10 min", body: "Each learner describes one situation from their own life — any culture — where they either read the social atmosphere correctly or failed to. Then: does your culture have an equivalent of 空気を読む? What is different about the Japanese version?", note: "Do not cut this phase if time is short. It is the most important moment in the session." }, ], }, }, ]; const CATEGORIES = [ { num: "01", jp: "社会的慣行", en: "Social Practices", desc: "Ongoing cognitive orientations that govern how one communicates across contexts.", count: "3 schemas" }, { num: "02", jp: "物質的遺物", en: "Material Artifacts", desc: "Objects that carry worlds. The physical thing is the entry point into a schema that extends beyond it.", count: "2 schemas" }, { num: "03", jp: "言語文化", en: "Language Culture", desc: "How Japanese encodes social reality at the level of the language system itself.", count: "2 schemas" }, { num: "04", jp: "季節的儀式", en: "Seasonal Rituals", desc: "Schemas organised around time. The calendar as a cognitive and social structure.", count: "2 schemas" }, { num: "05", jp: "領域スキーマ", en: "Domain Schemas", desc: "The cultural logic of specific social worlds: the izakaya, the onsen, the professional kitchen.", count: "2 schemas" }, { num: "06", jp: "空間文化", en: "Spatial Culture", desc: "The cognitive and moral significance of place. Japanese social space is not neutral.", count: "2 schemas" }, ]; export default function Kalcha() { const [screen, setScreen] = useState("browse"); const [activeSchema, setActiveSchema] = useState(null); const [weStage, setWeStage] = useState(0); const [activeTab, setActiveTab] = useState("words"); const [ankiIndex, setAnkiIndex] = useState(0); const [ankiFlipped, setAnkiFlipped] = useState(false); const [expandedVocab, setExpandedVocab] = useState(null); const [taskAnswers, setTaskAnswers] = useState({}); const [taskRevealed, setTaskRevealed] = useState({}); const timerRef = useRef(null); const schema = activeSchema ? SCHEMAS.find(s => s.id === activeSchema) : SCHEMAS[0]; // World entry timed sequence useEffect(() => { if (screen !== "world-entry") return; setWeStage(0); const timings = [600, 2200, 4200, 6200, 9000, 11000, 12500, 14000, 16000]; const timers = timings.map((t, i) => setTimeout(() => setWeStage(i + 1), t) ); return () => timers.forEach(clearTimeout); }, [screen]); const enterSchema = (id) => { setActiveSchema(id); setWeStage(0); setScreen("world-entry"); setAnkiIndex(0); setAnkiFlipped(false); setExpandedVocab(null); setTaskAnswers({}); setTaskRevealed({}); }; const visible = (n) => weStage >= n ? "visible" : ""; const nextAnki = () => { setAnkiFlipped(false); setTimeout(() => setAnkiIndex(i => (i + 1) % schema.vocab.length), 150); }; return ( <>
{/* NAV */} {/* ==================== BROWSE ==================== */} {screen === "browse" && (
Cultural Schema Tool · Japanese Language Acquisition
カルチャ
World before words.
A tool that builds the cognitive world of Japanese culture before language instruction begins. Select a schema to enter its world, then receive the language it generates.
{CATEGORIES.map(cat => ( ))}
Available Schemas · 10 entries
{SCHEMAS.map(s => (
enterSchema(s.id)}>
{s.jp}
{s.reading} {s.gloss}
{s.category}
))}
)} {/* ==================== WORLD ENTRY ==================== */} {screen === "world-entry" && schema && (
= 1 ? "loaded" : ""}`}>
出会い · Encounter · {schema.category}
{schema.schema.split('. ')[0]}.
{schema.schema.split('. ')[1]}.
{schema.schema.split('. ').slice(2).join('. ')}
{schema.jp}
{schema.reading}
{schema.gloss}
{schema.socialLogic}
{schema.cluster.map(c => ( {c} ))}
)} {/* ==================== LANGUAGE OUTPUT ==================== */} {screen === "language" && schema && (
setScreen("world-entry")}> ← {schema.jp} · World Entry
{schema.jp}
{schema.reading} — {schema.gloss}
{schema.socialLogic}
Language Output · {schema.vocab.length} items · sequenced by social function
{schema.vocab.map((v, i) => (
setExpandedVocab(expandedVocab === i ? null : i)} >
0{i + 1} {v.jp} {v.reading} {v.en}
{expandedVocab === i && (
{v.note}
)} {expandedVocab !== i && (
{v.note.substring(0, 80)}…
)}
))}
)} {/* ==================== PRACTICE ==================== */} {screen === "practice" && schema && (
setScreen("language")}> ← {schema.jp} · Language
{schema.jp}
{schema.gloss}
{[ { id: "words", label: "Word Practice" }, { id: "task", label: "Task" }, { id: "lesson", label: "Lesson Module" }, ].map(tab => ( ))}
{/* WORD PRACTICE */} {activeTab === "words" && (
{schema.vocab.map((_, i) => (
))}
setAnkiFlipped(!ankiFlipped)}>
{!ankiFlipped ? ( <>
{schema.vocab[ankiIndex].jp}
tap to reveal
) : (
{schema.vocab[ankiIndex].jp}
{schema.vocab[ankiIndex].reading}
{schema.vocab[ankiIndex].en}
{schema.vocab[ankiIndex].note}
)}
{ankiFlipped && (
)}

{ankiIndex + 1} of {schema.vocab.length} · tap card to flip

)} {/* TASK */} {activeTab === "task" && (

{schema.task.intro}

{schema.task.scenarios.map((scenario, si) => (
{scenario.label}
{scenario.lines.map((line, li) => (
{line.speaker} {line.text}
))}
{scenario.question}