diff --git a/frontend/src/index.css b/frontend/src/index.css index 1324cc5..f83fac1 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,2865 +1,28 @@ -:root { - --bg: #0a0a0f; - --bg-panel: #131320; - --bg-panel-glass: rgba(19, 19, 32, 0.82); - --bg-input: #1a1a2a; - --border: #2b2b3d; - --border-bright: #3d3d55; - --text: #e2ddd0; - --text-dim: #918c7d; - --accent: #d4a94e; - --accent-bright: #e8c476; - --accent-dim: #96773a; - --accent-glow: rgba(212, 169, 78, 0.25); - --danger: #d06565; - --player: #9fc7d1; - /* Chart hues (analytics dashboard). Deeper and more saturated than --accent - and --player, which are tuned for text and borders and turn muddy once - they are a 10px bar: these are checked against --bg-panel for lightness, - chroma, contrast and colour-blind separation as a set. */ - --chart-1: #b58a30; - --chart-2: #3d8ac4; - --chart-3: #a8608f; - --font-display: 'Cinzel', Georgia, serif; - --font-story: 'Crimson Pro', Georgia, 'Times New Roman', serif; - --font-ui: 'Inter', 'Segoe UI', system-ui, sans-serif; - font-family: var(--font-ui); - color-scheme: dark; -} - -* { box-sizing: border-box; } - -body { - margin: 0; - background: - radial-gradient(1200px 700px at 15% -10%, rgba(212, 169, 78, 0.06), transparent 60%), - radial-gradient(1000px 600px at 90% 110%, rgba(88, 76, 140, 0.08), transparent 55%), - var(--bg); - background-attachment: fixed; - color: var(--text); - min-height: 100vh; -} - -#root { min-height: 100vh; display: flex; flex-direction: column; } - -h1, h2, h3 { font-weight: 600; letter-spacing: 0.03em; } -a { color: var(--accent); text-decoration: none; transition: color 0.15s; } -a:hover { color: var(--accent-bright); } - -::selection { background: var(--accent-dim); color: #0f0d08; } - -/* ---------- Scrollbars ---------- */ - -::-webkit-scrollbar { width: 10px; height: 10px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { - background: var(--border-bright); - border-radius: 5px; - border: 2px solid var(--bg); -} -::-webkit-scrollbar-thumb:hover { background: var(--accent-dim); } - -/* ---------- Top navigation ---------- */ - -.topnav { - display: flex; - align-items: center; - gap: 26px; - padding: 0 28px; - height: 56px; - background: var(--bg-panel-glass); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border-bottom: 1px solid var(--border); - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); - position: sticky; - top: 0; - z-index: 10; -} -.topnav .brand { - font-family: var(--font-display); - font-size: 1.2rem; - font-weight: 700; - color: var(--accent); - letter-spacing: 0.14em; - text-shadow: 0 0 18px var(--accent-glow); - margin-right: 6px; -} -.topnav a.navlink { - color: var(--text-dim); - padding: 17px 2px 15px; - font-size: 0.9rem; - font-weight: 500; - letter-spacing: 0.04em; - border-bottom: 2px solid transparent; - transition: color 0.15s, border-color 0.2s; -} -.topnav a.navlink.active { - color: var(--accent-bright); - border-bottom-color: var(--accent); - text-shadow: 0 0 14px var(--accent-glow); -} -.topnav a.navlink:hover { color: var(--text); } - -/* On desktop the wrapper is transparent — its children (nav links + account) - participate directly in the .topnav flex row, exactly as before. The mobile - query below turns it into a dropdown. */ -.nav-links { display: contents; } -.nav-hamburger { - display: none; - background: none; - border: none; - color: var(--text); - font-size: 1.2rem; - padding: 6px 8px; - margin-left: auto; - line-height: 1; -} - -/* Phase 8: account area (hosted multi-user mode only) */ -.nav-account { - margin-left: auto; - display: flex; - align-items: center; - gap: 10px; - min-width: 0; -} -.nav-account button { padding: 5px 12px; font-size: 0.82rem; } -.guest-nudge { - color: var(--text-dim); - font-size: 0.78rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.account-email { - color: var(--text-dim); - font-size: 0.82rem; - max-width: 220px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -button.linklike { - background: none; - border: none; - padding: 0; - color: var(--accent); - font-size: 0.82rem; - cursor: pointer; -} -button.linklike:hover { text-decoration: underline; } -@media (max-width: 900px) { - .guest-nudge { display: none; } -} - -/* Phase 8: demo-key banner (Settings) */ -.demo-banner { - border: 1px solid var(--border-bright); - border-left: 3px solid var(--accent); - border-radius: 8px; - padding: 12px 16px; - margin-bottom: 18px; - color: var(--text-dim); - font-size: 0.88rem; - line-height: 1.5; -} -.demo-banner strong { color: var(--accent-bright); } - -.page { - flex: 1; - width: 100%; - max-width: 960px; - margin: 0 auto; - padding: 28px 24px; - animation: page-in 0.35s ease both; -} -@keyframes page-in { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: none; } -} - -/* ---------- Forms ---------- */ - -label.field { display: block; margin-bottom: 14px; } -label.field .label { - display: block; - font-size: 0.72rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.12em; - color: var(--text-dim); - margin-bottom: 5px; -} -input[type="text"], input[type="email"], input[type="password"], input[type="number"], textarea, select { - width: 100%; - background: var(--bg-input); - color: var(--text); - border: 1px solid var(--border); - border-radius: 8px; - padding: 9px 12px; - font-size: 0.95rem; - font-family: inherit; - transition: border-color 0.15s, box-shadow 0.15s; -} -textarea { resize: vertical; min-height: 70px; line-height: 1.5; } -input:focus, textarea:focus, select:focus { - outline: none; - border-color: var(--accent-dim); - box-shadow: 0 0 0 3px rgba(212, 169, 78, 0.12); -} -input::placeholder, textarea::placeholder { color: #5c584d; } - -button { - background: var(--bg-input); - color: var(--text); - border: 1px solid var(--border); - border-radius: 8px; - padding: 7px 16px; - font-size: 0.88rem; - font-weight: 500; - cursor: pointer; - font-family: inherit; - transition: border-color 0.15s, background 0.15s, box-shadow 0.15s, transform 0.1s; -} -button:hover { - border-color: var(--accent-dim); - box-shadow: 0 0 12px rgba(212, 169, 78, 0.1); -} -button:active { transform: translateY(1px); } -button.primary { - background: linear-gradient(160deg, var(--accent-bright), var(--accent) 55%, #b78d3e); - border-color: var(--accent); - color: #191307; - font-weight: 600; - letter-spacing: 0.02em; - box-shadow: 0 2px 10px var(--accent-glow); -} -button.primary:hover { - box-shadow: 0 2px 18px rgba(212, 169, 78, 0.45); - filter: brightness(1.08); -} -button.danger { color: var(--danger); } -button.danger:hover { border-color: var(--danger); box-shadow: 0 0 12px rgba(208, 101, 101, 0.15); } -button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: none; } - -/* ---------- Cards & lists ---------- */ - -.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; } -.card { - position: relative; - background: linear-gradient(170deg, #17172400, #17172400 60%, rgba(212, 169, 78, 0.03)), var(--bg-panel); - border: 1px solid var(--border); - border-radius: 12px; - padding: 18px; - cursor: pointer; - transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s; - overflow: hidden; -} -.card::before { - content: ''; - position: absolute; - inset: 0 0 auto 0; - height: 2px; - background: linear-gradient(90deg, transparent, var(--accent-dim), transparent); - opacity: 0; - transition: opacity 0.25s; -} -.card:hover { - border-color: var(--border-bright); - transform: translateY(-3px); - box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45), 0 0 20px rgba(212, 169, 78, 0.06); -} -.card:hover::before { opacity: 1; } -.card h3 { - margin: 0 0 6px; - font-family: var(--font-display); - font-size: 1.02rem; - color: var(--accent); -} -.card p { margin: 0; color: var(--text-dim); font-size: 0.88rem; line-height: 1.5; } -.card .meta { margin-top: 12px; font-size: 0.75rem; color: var(--text-dim); } - -.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } -.page-header h1 { - margin: 0; - font-family: var(--font-display); - font-size: 1.55rem; - color: var(--accent-bright); - text-shadow: 0 0 24px var(--accent-glow); -} - -.empty { color: var(--text-dim); text-align: center; padding: 60px 0; font-style: italic; font-family: var(--font-story); font-size: 1.05rem; } - -/* ---------- Story card editor ---------- */ - -.storycard { - background: var(--bg-input); - border: 1px solid var(--border); - border-left: 3px solid var(--accent-dim); - border-radius: 8px; - padding: 12px; - margin-bottom: 10px; - transition: border-color 0.15s; -} -.storycard:focus-within { border-color: var(--accent-dim); } -.storycard .row { display: flex; gap: 10px; margin-bottom: 8px; } -.storycard .row > * { flex: 1; } - -/* ---------- Play page ---------- */ - -.play-page { - display: flex; - flex-direction: column; - min-height: calc(100dvh - 56px); - /* Room to scroll the last beat clear of a composer lifted by the keyboard - (see --kb-inset in keyboard.js); 0 on desktop and on browsers that shrink - the layout viewport themselves. Margin, not padding — .page already sets - padding and a longhand here would drop its bottom gutter. */ - margin-bottom: var(--kb-inset, 0px); -} - -/* Keep the Plot/Memory/Scripts/Insights toggles reachable while the story - scrolls — pin the header just below the 56px top nav. */ -.play-page .page-header { - position: sticky; - top: 56px; - z-index: 5; - background: var(--bg); - margin-bottom: 12px; - padding: 12px 0; - border-bottom: 1px solid var(--border); -} - -.story { - flex: 1; - font-family: var(--font-story); - font-size: 1.14rem; - line-height: 1.7; - white-space: pre-wrap; - padding-bottom: 16px; -} -/* The head of a windowed transcript: how much story is still above, and the - way back to it. Deliberately quiet — it is a seam in the page, not a - feature — and it sits inside .story so it inherits the story face. */ -.story-earlier { - text-align: center; - margin: 4px 0 22px; - font-size: 0.9rem; - /* No action-in animation here: this appears above content the reader is - already looking at, and a fade would read as the story moving. */ -} -.story-earlier .dim { color: var(--text-dim); font-style: italic; } -.story-earlier button { - background: none; - border: none; - border-bottom: 1px solid rgba(159, 199, 209, 0.3); - color: var(--text-dim); - font: inherit; - font-style: italic; - cursor: pointer; - padding: 2px 4px; -} -.story-earlier button:hover { - color: var(--accent-bright); - border-bottom-color: var(--accent-bright); -} -.story .action { - position: relative; - margin-bottom: 15px; - padding-right: 56px; - animation: action-in 0.4s ease both; -} -@keyframes action-in { - from { opacity: 0; transform: translateY(6px); } - to { opacity: 1; transform: none; } -} -.story .action.player { - color: var(--player); - border-left: 2px solid rgba(159, 199, 209, 0.35); - padding-left: 14px; - font-style: italic; -} -.story .action.streaming { color: var(--text); } -.story .cursor { - color: var(--accent-bright); - text-shadow: 0 0 8px var(--accent-glow); - animation: blink 1s steps(1) infinite; -} -@keyframes blink { 50% { opacity: 0; } } - -/* Reasoning-model thinking, collapsed above the AI's response */ -.story .reasoning { - margin: 0 0 8px; - font-size: 0.82rem; - line-height: 1.5; - color: var(--text-dim); - font-style: normal; -} -.story .reasoning summary { - cursor: pointer; - user-select: none; - color: var(--text-dim); - opacity: 0.8; -} -.story .reasoning summary:hover { opacity: 1; } -.story .reasoning .reasoning-text { - margin-top: 6px; - padding: 8px 12px; - border-left: 2px solid var(--border); - opacity: 0.85; - max-height: 260px; - overflow-y: auto; -} - -.story .action-tools { - position: absolute; - top: 0; - right: 0; - display: inline-flex; - gap: 4px; - opacity: 0; - pointer-events: none; - transition: opacity 0.15s; -} -.story .action:hover .action-tools { opacity: 1; pointer-events: auto; } -.story .action-tools button { - padding: 1px 8px; - font-size: 0.72rem; - font-family: var(--font-ui); - color: var(--text-dim); - background: var(--bg-panel-glass); - backdrop-filter: blur(6px); -} -.story .action-tools button:hover { color: var(--text); } - -/* The takes of one turn: ‹ 2/4 ›. Stays quiet until hovered — it's a footnote - on the message, not part of the prose. - - Small and unemphatic on purpose. Stepping through takes reads them and does - nothing else, so it should not carry the visual weight of a decision; the - decision is made by writing, which happens in the composer below. */ -.take-pager { - display: flex; - align-items: center; - gap: 2px; - margin-top: 6px; - font-family: var(--font-ui); - opacity: 0.45; - transition: opacity 0.15s; -} -.story .action:hover .take-pager, -.take-pager:focus-within { opacity: 1; } -.take-pager button { - width: 20px; - height: 20px; - padding: 0; - font-size: 0.9rem; - line-height: 1; - color: var(--text-dim); - background: transparent; - border: 1px solid transparent; - border-radius: 4px; -} -.take-pager button:hover:not(:disabled) { - color: var(--text); - border-color: var(--border-bright); -} -.take-pager button:disabled { opacity: 0.3; cursor: default; } -.take-count { - min-width: 30px; - text-align: center; - font-size: 0.72rem; - font-variant-numeric: tabular-nums; - color: var(--text-dim); -} -/* Only while a take that isn't the live one is on screen: it says what the - next thing typed will do, because that is the click that forks. */ -.take-note { - margin-left: 6px; - font-size: 0.72rem; - font-style: italic; - color: var(--accent-dim); -} - -/* ---------- Branches panel (Phase 14, SP7) ---------- */ - -.panel-empty { - padding: 18px 0; - color: var(--text-dim); - font-size: 0.85rem; -} -.branch-panel { display: flex; flex-direction: column; gap: 14px; } -.branch-intro { - margin: 0; - color: var(--text-dim); - font-size: 0.82rem; - line-height: 1.55; -} -.branch-list { display: flex; flex-direction: column; gap: 8px; } -.branch-row { - display: flex; - flex-direction: column; - gap: 5px; - padding: 9px 11px; - border: 1px solid var(--border); - border-radius: 5px; - background: var(--bg-input); -} -.branch-row.here { - border-color: var(--accent-dim); - background: rgba(212, 169, 78, 0.07); -} -.branch-head { display: flex; align-items: center; gap: 7px; } -.branch-glyph { color: var(--border-bright); font-size: 0.85rem; } -.branch-row.here .branch-glyph { color: var(--accent); } -.branch-name { - font-family: var(--font-story); - font-size: 0.98rem; - color: var(--text); -} -.branch-row.here .branch-name { color: var(--accent-bright); } -.branch-here { - margin-left: auto; - font-size: 0.64rem; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--accent-dim); -} -.branch-rename { - flex: 1; - min-width: 0; - padding: 3px 7px; - font-family: var(--font-story); - font-size: 0.95rem; -} -.branch-meta { - font-size: 0.72rem; - color: var(--text-dim); - font-variant-numeric: tabular-nums; -} -.branch-tools, .branch-confirm { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; } -.branch-tools button, .branch-confirm button { - padding: 3px 9px; - font-size: 0.72rem; - color: var(--text-dim); - background: transparent; - border: 1px solid var(--border); -} -.branch-tools button:hover:not(:disabled), -.branch-confirm button:hover:not(:disabled) { - color: var(--accent-bright); - border-color: var(--border-bright); -} -.branch-tools button.danger:hover:not(:disabled), -.branch-confirm button.danger:hover:not(:disabled) { - color: var(--danger); - border-color: var(--danger); -} -.branch-tools button:disabled, .branch-confirm button:disabled { opacity: 0.4; cursor: default; } -.branch-confirm span { - font-size: 0.74rem; - color: var(--danger); - flex-basis: 100%; -} - -.action-edit { margin-bottom: 14px; } -/* Sized by AutoTextarea to fit the text being edited — an AI beat is usually - several paragraphs, and the old fixed 110px turned that into a keyhole. - min-height keeps a short player action from collapsing; max-height stops a - very long beat from pushing Save/Cancel off the screen (it scrolls instead). - resize stays on, so the height is a starting point, not a cage. */ -.action-edit textarea { - min-height: 110px; - max-height: 65vh; - overflow-y: auto; - font-family: inherit; - font-size: inherit; -} -.action-edit .edit-buttons { display: flex; gap: 8px; margin-top: 6px; } - -.play-controls { - position: sticky; - /* Sits on the keyboard's top edge when one is open, on the viewport bottom - otherwise — --kb-inset is 0 unless a virtual keyboard is covering us. */ - bottom: var(--kb-inset, 0px); - background: linear-gradient(to top, var(--bg) 75%, transparent); - padding: 18px 0 16px; - border-top: 1px solid var(--border); -} -.turn-buttons { display: flex; gap: 8px; margin-bottom: 10px; } -.turn-buttons button { font-size: 0.78rem; padding: 4px 12px; color: var(--text-dim); } -.turn-buttons button:hover:not(:disabled) { color: var(--text); } - -.input-bar { display: flex; gap: 8px; align-items: flex-end; } -.input-bar input { flex: 1; } -/* Auto-growing action box: starts at one line, grows to ~4, then scrolls. */ -.input-bar textarea { - flex: 1; - resize: none; - min-height: 0; - max-height: calc(6em + 20px); /* ~4 lines (1.5em each) + padding/border */ - overflow-y: auto; - line-height: 1.5; -} - -.mode-select { display: flex; } -.mode-select button { - border-radius: 0; - border-right-width: 0; - padding: 7px 14px; - color: var(--text-dim); - text-transform: capitalize; -} -.mode-select button:first-child { border-radius: 8px 0 0 8px; } -.mode-select button:last-child { border-radius: 0 8px 8px 0; border-right-width: 1px; } -.mode-select button.active { - color: var(--accent-bright); - background: var(--bg-panel); - border-color: var(--accent-dim); - border-right-width: 1px; - box-shadow: inset 0 0 12px rgba(212, 169, 78, 0.08); -} -.mode-select button.active + button { border-left-width: 0; } - -/* ---------- Side panel (Plot / Insights) ---------- */ - -.play-layout { flex: 1; display: flex; min-height: calc(100dvh - 56px); } -.play-layout .play-page { max-width: 960px; } -.play-layout.with-panel .play-page { max-width: none; } - -.panel-toggles { display: flex; gap: 8px; } -.panel-toggles button.active { - color: var(--accent-bright); - border-color: var(--accent-dim); - box-shadow: inset 0 0 12px rgba(212, 169, 78, 0.08); -} - -.side-panel { - width: 420px; - flex-shrink: 0; - border-left: 1px solid var(--border); - background: var(--bg-panel); - padding: 20px; - overflow-y: auto; - height: calc(100dvh - 56px); - position: sticky; - top: 56px; - animation: panel-in 0.25s ease both; -} - -/* ---------- In-play script viewer ---------- */ - -.adv-script { - border-bottom: 1px solid var(--border); - padding: 10px 0; -} -.adv-script-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; } -.adv-script-desc { font-size: 0.82rem; margin-top: 4px; } -.linklike { - border: none; - background: none; - padding: 0; - color: var(--accent-bright); - font-size: 0.8rem; - cursor: pointer; - flex-shrink: 0; -} -.linklike:hover { text-decoration: underline; } -.adv-script-code { margin-top: 8px; } -.adv-script-hook { - font-size: 0.72rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-dim); - margin: 8px 0 2px; -} -.adv-script-code pre { - margin: 0; - padding: 10px; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 6px; - overflow-x: auto; - font-family: var(--font-mono, monospace); - font-size: 0.76rem; - line-height: 1.5; - white-space: pre; -} - -/* ---------- Left status drawer (script state) ---------- */ - -.status-drawer { - flex-shrink: 0; - position: sticky; - top: 56px; - height: calc(100dvh - 56px); - display: flex; - align-items: flex-start; - border-right: 1px solid var(--border); - background: var(--bg-panel); -} -.status-drawer.open { width: 300px; } -.status-toggle { - flex-shrink: 0; - height: 100%; - width: 30px; - padding: 12px 0; - border: none; - border-radius: 0; - background: transparent; - color: var(--text-dim); - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; -} -.status-toggle:hover { color: var(--text); background: var(--bg-input); } -.status-toggle-label { - writing-mode: vertical-rl; - text-orientation: mixed; - letter-spacing: 0.12em; - font-size: 0.72rem; - text-transform: uppercase; -} -.status-body { - flex: 1; - min-width: 0; - height: 100%; - overflow-y: auto; - padding: 20px 16px; - animation: panel-in 0.2s ease both; -} -.status-vars { list-style: none; margin: 0; padding: 0; } -.status-vars li { - display: flex; - flex-direction: column; - gap: 2px; - padding: 8px 0; - border-bottom: 1px solid var(--border); -} -.status-key { - font-family: var(--font-mono, monospace); - font-size: 0.82rem; - color: var(--accent-bright); - word-break: break-word; -} -.status-val { - font-family: var(--font-mono, monospace); - font-size: 0.82rem; - color: var(--text); - white-space: pre-wrap; - word-break: break-word; -} -/* Recursive script-state value tree */ -.jv-string { color: var(--text); } -.jv-number { color: var(--player); } -.jv-bool { color: var(--accent-bright); } -.jv-null { color: var(--text-dim); font-style: italic; } -.jv-empty { color: var(--text-dim); } -.jv-tree { display: inline-block; vertical-align: top; max-width: 100%; } -.jv-toggle { - background: none; border: none; padding: 0; cursor: pointer; - font-family: inherit; font-size: 0.72rem; color: var(--text-dim); -} -.jv-toggle:hover { color: var(--accent-bright); } -.jv-children { - list-style: none; margin: 2px 0 2px 0; padding-left: 12px; - border-left: 1px solid var(--border); -} -.jv-children li { padding: 2px 0; word-break: break-word; } -.jv-key { color: var(--accent); margin-right: 6px; } -.jv-key::after { content: ':'; color: var(--text-dim); } -@keyframes panel-in { - from { opacity: 0; transform: translateX(16px); } - to { opacity: 1; transform: none; } -} - -/* ---------- World State drawer (Phase 12 RPG) ---------- */ -.ws-group { margin-bottom: 18px; } -.ws-group-title { - font-size: 0.72rem; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--text-dim); - margin: 0 0 8px; -} -.ws-stat { padding: 5px 0; } -.ws-stat-head { - display: flex; - justify-content: space-between; - align-items: baseline; - gap: 8px; - font-size: 0.85rem; -} -.ws-stat-name { color: var(--text); text-transform: capitalize; } -.ws-stat-val { - color: var(--text-dim); - font-variant-numeric: tabular-nums; - font-size: 0.8rem; - text-align: right; -} -.ws-band { color: var(--accent-bright); } -.ws-bar { - margin-top: 4px; - height: 5px; - border-radius: 3px; - background: var(--bg-input); - overflow: hidden; -} -.ws-bar-fill { - height: 100%; - border-radius: 3px; - background: var(--accent); - transition: width 0.35s ease; -} -/* Cast — each NPC is a small plate inside the one "Cast" group, so a roster of - five doesn't read as five more unrelated sections in the rail. */ -.ws-npc { - padding: 7px 9px; - margin-bottom: 7px; - border: 1px solid var(--border); - border-left: 2px solid var(--accent-dim); - border-radius: 7px; - background: rgba(255, 255, 255, 0.022); -} -.ws-npc:last-child { margin-bottom: 0; } -.ws-npc-head { display: flex; align-items: center; gap: 7px; margin-bottom: 3px; } -.ws-npc-avatar { - flex-shrink: 0; - width: 22px; height: 22px; - border-radius: 50%; - display: grid; place-items: center; - font-family: var(--font-display); - font-size: 0.6rem; - color: #14100a; - background: linear-gradient(150deg, var(--accent-bright), var(--accent-dim)); -} -.ws-npc-name { - font-family: var(--font-display); - font-size: 0.85rem; - color: var(--text); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.ws-none { font-size: 0.78rem; color: var(--text-dim); font-style: italic; } -.ws-milestones { list-style: none; margin: 0; padding: 0; } -.ws-milestones li { - display: flex; - gap: 7px; - align-items: flex-start; - padding: 4px 0; - font-size: 0.85rem; - color: var(--text); -} -.ws-milestones li.done { color: var(--text-dim); } -.ws-check { flex-shrink: 0; color: var(--accent-bright); } -.ws-milestones li.done .ws-check { color: var(--player); } -.ws-flags { list-style: none; margin: 0; padding: 0; } -.ws-flags li { - display: flex; - align-items: center; - gap: 8px; - padding: 4px 0; - font-size: 0.85rem; - color: var(--text); -} -.ws-flag-dot { - width: 9px; height: 9px; border-radius: 50%; - flex-shrink: 0; - background: var(--bg-input); - border: 1px solid var(--border); -} -.ws-flag-dot.on { background: var(--accent); border-color: var(--accent); } -.ws-flag-name { flex: 1; } -.ws-flag-val { color: var(--text-dim); font-size: 0.8rem; } -.ws-flag-edit { display: flex; align-items: center; gap: 8px; cursor: pointer; width: 100%; } - -.ws-edit-actions { display: flex; gap: 6px; } -.ws-edit-input { - width: 100%; - margin-top: 3px; - padding: 4px 7px; - font-size: 0.82rem; - border-radius: 6px; -} - -.schema-editor { - width: 100%; - font-family: var(--font-mono, monospace); - font-size: 0.82rem; - line-height: 1.5; - tab-size: 2; - white-space: pre; - overflow-x: auto; -} -.schema-error { - margin-top: 6px; - color: var(--danger, #e5484d); - font-size: 0.82rem; -} -.schema-json-actions { - display: flex; - align-items: center; - gap: 12px; - margin-top: 8px; -} -.schema-json-actions .schema-error { margin-top: 0; } -.schema-preview { - margin-top: 14px; - padding: 14px 16px; - border: 1px solid var(--border); - border-radius: 8px; - background: var(--bg-panel); -} -.schema-preview-title { - font-size: 0.72rem; - text-transform: uppercase; - letter-spacing: 0.12em; - color: var(--text-dim); - margin-bottom: 10px; -} -.schema-group { margin-bottom: 16px; } -.schema-group:last-child { margin-bottom: 0; } -.schema-group-title { - font-family: Georgia, serif; - font-size: 0.98rem; - color: var(--accent-bright); - margin-bottom: 6px; -} -.schema-npc-id { - font-family: var(--font-mono, monospace); - font-size: 0.72rem; - color: var(--text-dim); - margin-left: 6px; -} -.schema-stat { padding: 4px 0; border-bottom: 1px solid var(--border); } -.schema-stat:last-child { border-bottom: none; } -.schema-stat-head { - display: flex; - justify-content: space-between; - align-items: baseline; - gap: 12px; -} -.schema-stat-name { color: var(--text); text-transform: capitalize; } -.schema-stat-meta { - color: var(--text-dim); - font-size: 0.8rem; - font-variant-numeric: tabular-nums; - text-align: right; -} -.schema-desc { color: var(--text-dim); font-size: 0.8rem; margin-top: 2px; } -.schema-bands { color: var(--text-dim); font-size: 0.76rem; margin-top: 2px; font-style: italic; } - -/* ---- Form-based schema editor ---- - One shape for everything: a section is a panel, every entry inside it is a - `.se-row` tile, and every control wears a `.se-field` caption. Adding a new - kind of entry means reusing those three, not inventing a fourth box. */ -.se-section { - margin-bottom: 16px; - padding: 13px 15px; - border: 1px solid var(--border); - border-radius: 10px; - background: var(--bg-panel); -} -.se-section-nested { - margin: 10px 0 0; - padding: 11px 12px; - background: rgba(0, 0, 0, 0.22); - border-radius: 8px; -} -.se-section-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - margin-bottom: 8px; -} -.se-section-title { - font-size: 0.74rem; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--accent); -} -.se-hint { - margin: -2px 0 10px; - font-size: 0.78rem; - line-height: 1.45; - color: var(--text-dim); -} -.se-empty { color: var(--text-dim); font-size: 0.82rem; padding: 4px 0; } -.se-add, .se-add-sm { - font-size: 0.78rem; - padding: 3px 10px; - white-space: nowrap; -} -.se-add-sm { margin-top: 6px; } -.se-remove { - background: transparent; - border: none; - color: var(--text-dim); - cursor: pointer; - padding: 2px 6px; - font-size: 0.9rem; - line-height: 1; - flex-shrink: 0; -} -.se-remove:hover { color: var(--danger, #e5484d); } - -/* Every control in the editor, one look. The global field rule keys off - `input[type="…"]`, so the plain ``s here (keys, descriptions) never - matched it and fell back to browser defaults — that mismatch is most of why - this panel used to read as a pile of unrelated boxes. Everything below is - qualified with `.se` so it outranks that global rule (0,1,1) too. */ -/* `:where()` so the exclusion costs no specificity — a plain - `:not([type="checkbox"])` would rank this above the `.se .se-*` overrides - below and swallow every one of them. */ -.se input:not(:where([type="checkbox"])), .se textarea, .se select { - width: 100%; - background: var(--bg-input); - color: var(--text); - border: 1px solid var(--border); - border-radius: 6px; - padding: 5px 8px; - font-size: 0.84rem; - font-family: inherit; - transition: border-color 0.15s, box-shadow 0.15s; -} - -/* One entry — a stat, a flag, a milestone. */ -.se-row { - padding: 10px 12px; - margin-bottom: 8px; - border: 1px solid var(--border); - border-radius: 8px; - background: rgba(255, 255, 255, 0.018); - transition: border-color 0.15s; -} -.se-row:last-child { margin-bottom: 0; } -.se-row:focus-within { border-color: var(--border-bright); } -/* Wraps because a phone-width row can't fit key + toggle + ✕ side by side, and - an input's default `min-width: auto` would push the ✕ off-screen instead. */ -.se-row-top { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 8px; } -.se .se-key { - font-family: var(--font-mono, monospace); - font-size: 0.82rem; - min-width: 0; - max-width: 220px; -} -/* Only in the row context — `.se-field` is a *column* flex, where a basis of - 140px would set the input's height instead of its width. */ -.se .se-row-top .se-key { flex: 1 1 140px; } -.se-field { display: flex; flex-direction: column; gap: 3px; } -.se-field > span { - font-size: 0.66rem; - text-transform: uppercase; - letter-spacing: 0.09em; - color: var(--text-dim); -} -.se-fields { - display: flex; - flex-wrap: wrap; - gap: 8px 12px; - align-items: flex-end; - margin-bottom: 8px; -} -.se .se-num input, .se .se-num select { width: 72px; padding: 4px 6px; } -.se .se-kind select { width: 132px; } -.se .se-initial-text input { width: 220px; max-width: 100%; } -.se-check { - display: flex; align-items: center; gap: 5px; - font-size: 0.8rem; color: var(--text-dim); - white-space: nowrap; -} -.se-check input { width: auto; } -/* The global textarea rule also forces min-height:70px, which a 2-row inline - field doesn't want. */ -.se .se-area { min-height: 0; line-height: 1.45; resize: vertical; } -.se-sub-head { font-size: 0.7rem; color: var(--text-dim); margin: 4px 0 4px; } -.se-band { display: flex; gap: 6px; align-items: center; margin-bottom: 4px; } -/* Qualified so it outweighs the global input[type="number"] rule (0,1,1) — - as a bare class it lost, and the band bounds rendered full-width. */ -.se .se-band-n { width: 64px; padding: 4px 6px; } -.se .se-band-l { flex: 1 1 60px; width: auto; min-width: 0; } -.se-bands { margin-top: 8px; } - -/* ---- NPC roster (its own page section) ---- */ -.se-npc-grid { - display: grid; - /* min() so a 320px phone gets one column instead of a 340px overflow */ - grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr)); - gap: 14px; - align-items: start; -} -.se-npc { - border: 1px solid var(--border); - border-radius: 10px; - background: var(--bg-panel); - overflow: hidden; -} -.se-npc:focus-within { border-color: var(--border-bright); } -.se-npc-head { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 12px; - border-bottom: 1px solid var(--border); - background: linear-gradient(180deg, rgba(212, 169, 78, 0.07), transparent); -} -.se-avatar { - flex-shrink: 0; - width: 34px; height: 34px; - border-radius: 50%; - display: grid; place-items: center; - font-family: var(--font-display); - font-size: 0.8rem; - letter-spacing: 0.02em; - color: #14100a; - background: linear-gradient(150deg, var(--accent-bright), var(--accent-dim)); -} -.se-npc-ident { flex: 1; min-width: 0; } -.se .se-npc-name { - padding: 2px 6px; - font-family: var(--font-display); - font-size: 0.95rem; - background: transparent; - border-color: transparent; -} -.se .se-npc-name:focus { background: var(--bg-input); border-color: var(--border-bright); } -.se-npc-addr { - display: block; - margin: 1px 0 0 6px; - font-family: var(--font-mono, monospace); - font-size: 0.7rem; - color: var(--text-dim); -} -.se-npc-count { - flex-shrink: 0; - font-size: 0.7rem; - color: var(--text-dim); - white-space: nowrap; -} -.se-npc-body { padding: 12px; display: flex; flex-direction: column; gap: 10px; } -.se-npc-idfield .se-key { max-width: 100%; } -.ws-report { list-style: none; margin: 4px 0 0; padding: 0; font-size: 0.82rem; } -.ws-report li { padding: 2px 0; color: var(--text); } - -/* Inline per-turn change chips under an AI message */ -.turn-changes { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-top: 8px; -} -.chg { - font-size: 0.74rem; - padding: 2px 8px; - border-radius: 10px; - border: 1px solid var(--border); - background: var(--bg-input); - color: var(--text-dim); - font-variant-numeric: tabular-nums; - /* A chip must never be wider than the column it sits in: an NPC stat like - "bandit leader aggression +10" overruns a phone, and the chip row is - inside the story text, which has nothing to scroll. The label may break - mid-word as a last resort; the value is glued to it by its own nowrap so - the sign and number never end up on separate lines. */ - max-width: 100%; - overflow-wrap: anywhere; -} -.chg-val { white-space: nowrap; } -.chg-up { color: var(--player, #4caf82); border-color: color-mix(in srgb, var(--player, #4caf82) 40%, var(--border)); } -.chg-down { color: var(--danger, #e5484d); border-color: color-mix(in srgb, var(--danger, #e5484d) 40%, var(--border)); } -.chg-flag { color: var(--accent-bright); } -.chg-ms { color: var(--accent-bright); border-color: color-mix(in srgb, var(--accent-bright) 40%, var(--border)); } -/* A refused change, and a clamp that left the value where it started. The - dashed border separates it from the changes that took effect. It is not - styled as an error, because the rules refusing a change is them working. */ -.chg-refused { - border-style: dashed; - color: var(--text-dim); - border-color: color-mix(in srgb, var(--danger) 30%, var(--border)); -} -/* A clamp that reduced a change but still moved the value. */ -.chg-limited { opacity: 0.75; font-style: italic; } -.ws-report code { color: var(--accent-bright); } -.ws-delta { color: var(--player); font-variant-numeric: tabular-nums; } -.ws-flag { color: var(--text-dim); font-style: italic; } -.ws-rejected { color: var(--text-dim); } -.side-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } -.side-panel-header h2 { - margin: 0; - font-family: var(--font-display); - font-size: 1.05rem; - color: var(--accent); -} - -/* ---------- Insights ---------- */ - -.insights-meta { margin-bottom: 16px; font-size: 0.85rem; } -.insights-mode { color: var(--text-dim); margin-bottom: 8px; } -.insights-mode button { padding: 2px 8px; font-size: 0.75rem; } -.token-total { font-size: 1rem; color: var(--text); margin-bottom: 4px; } -.token-total.over { color: var(--danger); } -/* Stacked share-of-context bar. Segment colours come from SECTION_COLORS in - Play.jsx so the bar, its legend and each section header stay in sync. */ -.token-split { - display: flex; - height: 14px; - background: var(--bg-input); - border-radius: 4px; - overflow: hidden; - margin-bottom: 8px; -} -.token-split.over { box-shadow: 0 0 0 1px var(--danger); } -.token-seg { - min-width: 2px; /* a 0.2% section is still worth seeing */ - border-right: 1px solid var(--bg-panel); - transition: opacity 0.15s ease; -} -.token-seg:last-child { border-right: none; } -.token-seg-free { background: transparent; min-width: 0; border-right: none; } -.token-seg.faded, .token-legend-item.faded { opacity: 0.25; } - -.token-legend { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); - gap: 1px 12px; - margin-bottom: 10px; -} -.token-legend-item { - display: flex; - align-items: center; - gap: 7px; - background: none; - border: none; - border-radius: 4px; - padding: 2px 5px; - font-size: 0.78rem; - color: var(--text-dim); - text-align: left; - transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease; -} -.token-legend-item:hover { background: rgba(255, 255, 255, 0.05); color: var(--text); } -.token-swatch { width: 9px; height: 9px; border-radius: 2px; flex: none; } -.token-legend-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.token-legend-pct { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; } -.token-legend-tok { - width: 4.5em; - text-align: right; - opacity: 0.65; - font-variant-numeric: tabular-nums; -} -.token-legend-more { - justify-self: start; - background: none; - border: none; - padding: 2px 5px; - font-size: 0.75rem; - color: var(--text-dim); -} -.token-legend-more:hover { color: var(--accent); background: none; } -.insights-history { color: var(--text-dim); margin-bottom: 8px; } -.insights-cards div { margin-bottom: 3px; } -.insights-cards .dropped { color: var(--danger); } - -.ctx-section { margin-bottom: 12px; border-left: 3px solid var(--border); background: rgba(255, 255, 255, 0.015); border-radius: 0 6px 6px 0; } -.ctx-section .ctx-header { - display: flex; - justify-content: space-between; - font-size: 0.7rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--text-dim); - padding: 5px 10px 3px; -} -.ctx-section pre { - margin: 0; - padding: 6px 10px 8px; - white-space: pre-wrap; - word-break: break-word; - font-family: Consolas, monospace; - font-size: 0.8rem; - line-height: 1.5; - color: var(--text); - max-height: 260px; - overflow-y: auto; -} -/* Per-section border/header colours are applied inline from SECTION_COLORS - (Play.jsx) — one palette drives the bar, the legend and these headers. */ - -/* ---------- Scripts ---------- */ - -.slot-tabs { display: flex; gap: 0; margin-bottom: 4px; } -.slot-tabs button { - border-radius: 0; - border-right-width: 0; - color: var(--text-dim); -} -.slot-tabs button:first-child { border-radius: 8px 0 0 8px; } -.slot-tabs button:last-child { border-radius: 0 8px 8px 0; border-right-width: 1px; } -.slot-tabs button.active { color: var(--accent-bright); background: var(--bg-panel); } -.slot-hint { color: var(--text-dim); font-size: 0.8rem; margin-bottom: 8px; } - -.cm-editor { border: 1px solid var(--border); border-radius: 8px; font-size: 0.88rem; } -.cm-editor.cm-focused { - outline: none; - border-color: var(--accent-dim); - box-shadow: 0 0 0 3px rgba(212, 169, 78, 0.1); -} - -.test-run .row { display: flex; gap: 12px; } -.test-result { - margin-top: 12px; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 8px; - padding: 10px 12px; - font-size: 0.85rem; -} -.test-result .label { - text-transform: uppercase; - font-size: 0.7rem; - letter-spacing: 0.08em; - color: var(--text-dim); - margin-right: 6px; -} -.test-result pre { - display: inline-block; - margin: 2px 0; - white-space: pre-wrap; - word-break: break-word; - font-family: Consolas, monospace; - vertical-align: top; -} -.test-error { color: var(--danger); white-space: pre-wrap; } - -.script-attach { - display: flex; - align-items: baseline; - gap: 8px; - padding: 9px 12px; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 8px; - margin-bottom: 6px; - cursor: pointer; - transition: border-color 0.15s; -} -.script-attach:hover { border-color: var(--border-bright); } -.script-attach .dim { color: var(--text-dim); font-size: 0.82rem; } - -/* ---------- Memory bank (Phase 6) ---------- */ - -.memory-row { - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 8px; - padding: 9px 12px; - margin-bottom: 6px; -} -.memory-row.forgotten { opacity: 0.55; } -.memory-text { font-size: 0.9rem; white-space: pre-wrap; } -.memory-meta { - display: flex; - align-items: baseline; - flex-wrap: wrap; - gap: 10px; - margin-top: 5px; - font-size: 0.78rem; -} -.memory-meta .dim { color: var(--text-dim); } -.memory-meta .action-tools { display: inline-flex; gap: 4px; margin-left: auto; } -.memory-meta .action-tools button { - padding: 1px 8px; - font-size: 0.72rem; - color: var(--text-dim); -} -.memory-meta .action-tools button:hover { color: var(--text); } -.memory-badge { - color: var(--accent); - border: 1px solid var(--border); - border-radius: 999px; - padding: 0 8px; -} - -.script-report { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 8px; } -.script-error { color: var(--danger); font-size: 0.85rem; margin-bottom: 4px; white-space: pre-wrap; } -.script-logs { - background: var(--bg-input); - border-radius: 8px; - padding: 8px 10px; - font-family: Consolas, monospace; - font-size: 0.8rem; - white-space: pre-wrap; - word-break: break-word; - margin: 6px 0; -} - -/* ---------- Filter bar & tags ---------- */ - -.filter-bar { margin-bottom: 18px; } -.search-input { max-width: 340px; } -.tag-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; } -.tag { - padding: 2px 12px; - font-size: 0.76rem; - border-radius: 999px; - color: var(--text-dim); -} -.tag.active { - color: var(--accent-bright); - border-color: var(--accent-dim); - box-shadow: inset 0 0 10px rgba(212, 169, 78, 0.1); -} -.tag.small { - display: inline-block; - background: var(--bg-input); - border: 1px solid var(--border); - padding: 1px 9px; - margin-right: 4px; - cursor: default; -} - -/* ---------- Modal ---------- */ - -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(5, 5, 9, 0.7); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - z-index: 50; - animation: page-in 0.2s ease both; -} -.modal { - width: min(480px, calc(100vw - 40px)); - max-height: 80vh; - overflow-y: auto; - background: var(--bg-panel); - border: 1px solid var(--border-bright); - border-radius: 14px; - padding: 26px 28px; - box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6), 0 0 40px rgba(212, 169, 78, 0.06); -} -.modal h2 { - margin: 0 0 4px; - font-family: var(--font-display); - font-size: 1.2rem; - color: var(--accent-bright); -} -.modal .modal-hint { margin: 0 0 18px; color: var(--text-dim); font-size: 0.88rem; } -.modal .modal-buttons { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; } - -/* ---------- Auth (log in / sign up) ---------- */ - -.auth-modal { width: min(400px, calc(100vw - 40px)); text-align: center; padding-top: 22px; } -.auth-crest { - font-size: 1.1rem; - color: var(--accent-dim); - letter-spacing: 0.3em; - margin-bottom: 6px; -} -.auth-modal h2 { font-size: 1.35rem; } -.auth-modal .auth-hint { margin-bottom: 16px; line-height: 1.45; } -/* Fields go back to left-aligned — only the header is centred. */ -.auth-modal label.field { text-align: left; } - -.auth-tabs { - display: flex; - gap: 4px; - padding: 4px; - margin-bottom: 18px; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 10px; -} -.auth-tab { - flex: 1; - padding: 7px 0; - border: 1px solid transparent; - border-radius: 7px; - background: transparent; - color: var(--text-dim); - font-family: var(--font-display); - font-size: 0.78rem; - letter-spacing: 0.1em; - text-transform: uppercase; -} -.auth-tab:hover { color: var(--text); background: rgba(212, 169, 78, 0.06); } -.auth-tab.active { - background: var(--bg-panel); - border-color: var(--accent-dim); - color: var(--accent-bright); -} - -.auth-password { display: flex; gap: 8px; } -.auth-password input { flex: 1; min-width: 0; } -.auth-reveal { - flex: none; - padding: 0 12px; - font-size: 0.78rem; - color: var(--text-dim); -} -.auth-reveal:hover { color: var(--accent-bright); } - -.auth-help { - display: block; - margin-top: 6px; - font-size: 0.76rem; - color: var(--text-dim); -} -.auth-error { - margin-top: 4px; - padding: 9px 12px; - border: 1px solid rgba(208, 101, 101, 0.45); - border-radius: 8px; - background: rgba(208, 101, 101, 0.09); - color: var(--danger); - font-size: 0.85rem; - text-align: left; - white-space: pre-wrap; -} - -/* "Update from scenario" — a wider modal, since it lists a diff. */ -.refresh-modal { width: min(620px, calc(100vw - 40px)); } -.refresh-changes { - border: 1px solid var(--border); - border-radius: 10px; - padding: 12px 14px; - font-size: 0.86rem; -} -.refresh-change + .refresh-change { - margin-top: 10px; - padding-top: 10px; - border-top: 1px solid var(--border); -} -.refresh-old, .refresh-new { margin-top: 3px; font-family: var(--font-mono, monospace); font-size: 0.8rem; } -.refresh-old { text-decoration: line-through; } -.refresh-new { color: var(--accent-bright); } -/* The button sits above the plot fields it would overwrite. */ -.plot-source { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 12px; - flex-wrap: wrap; - margin-bottom: 16px; - padding-bottom: 12px; - border-bottom: 1px solid var(--border); - font-size: 0.82rem; -} -.plot-source .linklike { flex-shrink: 0; } - -/* ---------- Debug log (Settings) ---------- */ - -.debug-entry { - border: 1px solid var(--border); - border-radius: 8px; - margin-bottom: 8px; - overflow: hidden; -} -.debug-entry-head { - display: flex; - align-items: baseline; - gap: 12px; - padding: 9px 12px; - cursor: pointer; - font-size: 0.86rem; - background: var(--bg-input); -} -.debug-entry-head .dim, .debug-entry-body .dim { color: var(--text-dim); font-size: 0.8rem; } -.debug-status { - font-size: 0.68rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; - padding: 1px 8px; - border-radius: 999px; - border: 1px solid var(--border-bright); - color: var(--text-dim); -} -.debug-status.ok { color: #7dc9a2; border-color: #7dc9a244; } -.debug-status.error { color: var(--danger); border-color: var(--danger); } -.debug-status.pending { color: var(--accent); border-color: var(--accent-dim); } -.debug-entry-body { padding: 10px 12px; font-size: 0.85rem; } -.debug-entry-body pre { - margin: 2px 0 8px; - padding: 8px 10px; - background: var(--bg-input); - border-radius: 6px; - white-space: pre-wrap; - word-break: break-word; - font-family: Consolas, monospace; - font-size: 0.78rem; - max-height: 300px; - overflow-y: auto; -} - -/* ---------- Play screen error/success banner ---------- - Persistent (no auto-dismiss) and can carry a Retry action, so it stays - separate from the transient global toasts further down this file. */ - -.play-toast { - position: fixed; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: var(--bg-panel-glass); - backdrop-filter: blur(10px); - border: 1px solid var(--danger); - color: var(--text); - padding: 11px 20px; - border-radius: 10px; - z-index: 100; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5); - animation: toast-in 0.25s ease both; -} -.play-toast.ok { border-color: var(--accent); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5), 0 0 20px var(--accent-glow); } -@keyframes toast-in { - from { opacity: 0; transform: translate(-50%, 12px); } - to { opacity: 1; transform: translate(-50%, 0); } -} - -/* ============================================================ - Illuminated tome — ambient layer, ornament, art plates, cards - Everything below styles the home/library surfaces. It comes after - the base .card rules on purpose: several selectors here tie with - earlier ones on specificity and rely on source order to win. - ============================================================ */ - -/* ---------- Ambient drifting motes ---------- */ - -/* Behind every page: fixed, unclickable, and out of the stacking flow. The - canvas is sized in JS (see Embers.jsx); z-index 0 with the app's own - surfaces at 1+ keeps it under content without needing a negative index - (which would drop it behind the body background). */ -.embers { - position: fixed; - inset: 0; - z-index: 0; - pointer-events: none; - opacity: 0.85; -} -/* .topnav already carries z-index 10 — deliberately not touched here, so it - keeps sitting above page content as well as above the canvas. */ -.page { position: relative; z-index: 1; } - -/* A third gradient pool warming the middle of the ground. `background` is a - shorthand, so background-attachment has to be restated or it resets to - scroll and the gradients start sliding with the page. */ -body { - background: - radial-gradient(1200px 700px at 15% -10%, rgba(212, 169, 78, 0.07), transparent 60%), - radial-gradient(1000px 600px at 90% 110%, rgba(88, 76, 140, 0.1), transparent 55%), - radial-gradient(760px 520px at 50% 42%, rgba(150, 119, 58, 0.05), transparent 62%), - var(--bg); - background-attachment: fixed; -} - -/* ---------- Home banner ---------- */ - -.hall { - display: flex; - flex-direction: column; - gap: 5px; - padding: 6px 0 26px; - margin-bottom: 4px; - border-bottom: 1px solid var(--border); - position: relative; -} -/* Gold hairline that fades out toward both ends, sat on the border above. */ -.hall::after { - content: ''; - position: absolute; - bottom: -1px; - left: 0; - right: 0; - height: 1px; - background: linear-gradient(90deg, transparent, var(--accent-dim) 22%, var(--accent-dim) 78%, transparent); - opacity: 0.55; -} -.hall-eyebrow { - margin: 0; - font-size: 0.68rem; - letter-spacing: 0.24em; - text-transform: uppercase; - color: var(--accent-dim); - font-weight: 600; -} -.hall-title { - margin: 0; - font-family: var(--font-display); - font-size: clamp(1.7rem, 4.6vw, 2.3rem); - font-weight: 600; - letter-spacing: 0.05em; - color: var(--accent-bright); - text-shadow: 0 0 30px var(--accent-glow); - text-wrap: balance; -} -.hall-sub { - margin: 0; - color: var(--text-dim); - font-family: var(--font-story); - font-size: 1.02rem; -} - -/* ---------- Ornamental section rules ---------- */ - -.home-section { margin-top: 34px; } - -.rule-head { - display: flex; - align-items: center; - gap: 16px; - margin-bottom: 16px; -} -.rule { - flex: 1; - display: flex; - align-items: center; - gap: 14px; - margin: 0; - font-family: var(--font-display); - font-size: 0.82rem; - font-weight: 600; - letter-spacing: 0.26em; - text-transform: uppercase; - color: var(--accent-bright); - white-space: nowrap; -} -/* Rules grow to fill whatever the label leaves — the label stays optically - centred in its own space without needing a fixed width. */ -.rule::before, .rule::after { - content: ''; - height: 1px; - flex: 1; - background: linear-gradient(90deg, transparent, var(--accent-dim) 45%, var(--accent-dim) 55%, transparent); -} -.rule::before { flex: 0 0 26px; } -.rule > span { flex: none; } - -button.linklike.ornate { - font-family: var(--font-display); - font-size: 0.74rem; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--accent-dim); - white-space: nowrap; - transition: color 0.15s; -} -button.linklike.ornate:hover { color: var(--accent-bright); text-decoration: none; } - -/* ---------- Art plates ---------- */ - -.art { - width: 38px; - height: 38px; - flex: none; - border-radius: 3px; - border: 1px solid var(--accent-dim); - box-shadow: inset 0 0 0 1px rgba(10, 10, 15, 0.7); - overflow: hidden; - display: grid; - place-items: center; - line-height: 1; -} -.art-lg { width: 46px; height: 46px; } -.art-photo img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} -.art-icon { font-size: 1.15rem; } -.art-lg.art-icon { font-size: 1.4rem; } -.art-mono { - font-family: var(--font-display); - font-size: 0.82rem; - font-weight: 700; - letter-spacing: 0.06em; - color: rgba(232, 196, 118, 0.92); - text-shadow: 0 1px 4px rgba(0, 0, 0, 0.65); -} -.art-lg.art-mono { font-size: 0.95rem; } - -/* ---------- Tome cards ---------- */ - -.card-grid.wide { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); } - -.card.tome { - border-radius: 3px; - padding: 16px 17px 14px; - background: - linear-gradient(168deg, rgba(212, 169, 78, 0.05), transparent 55%), - var(--bg-panel); - /* Inner dark gutter + a gold keyline: the double-rule border of a plate in - an old book, done with shadows so it costs no extra element. */ - box-shadow: - inset 0 0 0 1px rgba(10, 10, 15, 0.9), - inset 0 0 0 2px rgba(150, 119, 58, 0.22); -} - -/* Filigree corner brackets. These override the base .card::before gradient - bar (equal specificity, later in the file wins). */ -.card.tome::before, -.card.tome::after { - content: ''; - position: absolute; - width: 12px; - height: 12px; - border: 1px solid var(--accent-dim); - opacity: 0.5; - transition: opacity 0.2s; - background: none; - inset: auto; -} -.card.tome::before { - top: 6px; - left: 6px; - border-right: 0; - border-bottom: 0; -} -.card.tome::after { - bottom: 6px; - right: 6px; - border-left: 0; - border-top: 0; -} -.card.tome:hover::before, -.card.tome:hover::after { opacity: 1; } -.card.tome:hover { - border-color: var(--accent-dim); - box-shadow: - inset 0 0 0 1px rgba(10, 10, 15, 0.9), - inset 0 0 0 2px rgba(212, 169, 78, 0.4), - 0 12px 30px rgba(0, 0, 0, 0.5), - 0 0 26px rgba(212, 169, 78, 0.08); -} - -.card-head { display: flex; gap: 12px; align-items: center; margin-bottom: 10px; } -/* Keep headings clear of the wax seal, which is absolutely positioned over - the card's top-right corner. Only cards that actually carry one pay the - indent, so scenario cards keep their full width. */ -.card.tome:has(> .seal) .card-head { padding-right: 26px; } -.card-headings { min-width: 0; } -.card.tome h3 { - margin: 0; - font-family: var(--font-display); - font-size: 1rem; - font-weight: 600; - letter-spacing: 0.045em; - color: var(--accent); - overflow-wrap: anywhere; -} -.card-from { - margin: 3px 0 0; - font-size: 0.75rem; - color: var(--text-dim); - overflow-wrap: anywhere; -} - -/* Wax seal marking a story in progress. */ -.seal { - position: absolute; - top: 12px; - right: 12px; - width: 24px; - height: 24px; - border-radius: 50%; - background: radial-gradient(circle at 34% 30%, #b8553f, #7d2f24 72%); - box-shadow: 0 1px 6px rgba(0, 0, 0, 0.6), inset 0 -1px 3px rgba(0, 0, 0, 0.45); - display: grid; - place-items: center; - font-size: 0.6rem; - color: #f0d5b0; - z-index: 1; -} - -.snippet { - margin: 0; - font-family: var(--font-story); - font-size: 0.98rem; - line-height: 1.5; - color: #b3ac9c; - /* Clamp so a long beat can't stretch one card taller than its row. */ - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; -} -.snippet.muted { color: var(--text-dim); font-style: italic; } -.snippet.dropcap::first-letter { - float: left; - font-family: var(--font-display); - font-size: 2.3rem; - line-height: 0.85; - font-weight: 700; - color: var(--accent); - padding: 3px 7px 0 0; -} - -.card-foot { - display: flex; - align-items: center; - gap: 8px; - margin-top: 13px; - padding-top: 11px; - border-top: 1px solid var(--border); - font-size: 0.74rem; - color: var(--text-dim); - flex-wrap: wrap; -} -.card-foot .turns { font-variant-numeric: tabular-nums; } -.card-foot .dot { opacity: 0.5; } -.card-foot .resume { - margin-left: auto; - color: var(--accent-dim); - font-family: var(--font-ui); - letter-spacing: 0.1em; - text-transform: uppercase; - font-size: 0.68rem; - font-weight: 600; - transition: color 0.15s; -} -.card.tome:hover .resume { color: var(--accent-bright); } -.tag-cluster { display: flex; gap: 5px; flex-wrap: wrap; min-width: 0; } -.card-actions { margin-left: auto; display: inline-flex; gap: 5px; } - -button.tiny { padding: 2px 9px; font-size: 0.72rem; } -button.primary.compact { padding: 3px 12px; font-size: 0.76rem; margin-left: auto; } - -/* Staggered card entrance. The per-card delay is set inline. */ -.enter { animation: card-in 0.4s ease both; } -@keyframes card-in { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: none; } -} - -/* ---------- Loading skeletons ---------- */ - -.skeleton-card { - cursor: default; - pointer-events: none; - animation: card-in 0.3s ease both; -} -.skeleton-card::before, .skeleton-card::after { content: none; } -.skeleton-card:hover { transform: none; border-color: var(--border); box-shadow: none; } -.sk { - border-radius: 3px; - background: linear-gradient(90deg, #1c1c2b 25%, #262639 50%, #1c1c2b 75%); - background-size: 220% 100%; - animation: shimmer 1.4s ease-in-out infinite; -} -.sk-plate { width: 38px; height: 38px; margin-bottom: 12px; } -.sk-title { height: 13px; width: 58%; margin-bottom: 12px; } -.sk-line { height: 9px; margin-bottom: 8px; } -@keyframes shimmer { - from { background-position: 120% 0; } - to { background-position: -120% 0; } -} - -/* ---------- Global toasts ---------- */ - -.toast-host { - position: fixed; - bottom: 22px; - left: 50%; - transform: translateX(-50%); - z-index: 120; - display: flex; - flex-direction: column-reverse; - gap: 8px; - align-items: center; - width: max-content; - max-width: min(92vw, 520px); - pointer-events: none; -} -.toast { - pointer-events: auto; - display: flex; - align-items: flex-start; - gap: 9px; - text-align: left; - font-family: var(--font-ui); - font-size: 0.86rem; - line-height: 1.45; - padding: 10px 16px; - border-radius: 8px; - background: var(--bg-panel-glass); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--border-bright); - border-left: 3px solid var(--accent); - color: var(--text); - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5); - animation: rise 0.25s ease both; -} -.toast:hover { border-color: var(--accent-dim); } -.toast-error { border-left-color: var(--danger); } -.toast-mark { color: var(--accent); flex: none; line-height: 1.5; } -.toast-error .toast-mark { color: var(--danger); } -@keyframes rise { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: none; } -} - -/* ---------- Story text polish ---------- */ - -/* Drop cap opening the story's first narrated beat. */ -.story .action.opening::first-letter { - float: left; - font-family: var(--font-display); - font-size: 3.1rem; - line-height: 0.82; - font-weight: 700; - color: var(--accent); - padding: 5px 10px 0 2px; - text-shadow: 0 0 22px var(--accent-glow); -} - -/* Ornamental separator between turns — a diamond flanked by fading rules. - Marks the beat change without the heaviness of a full
. */ -.scene-break { - display: flex; - align-items: center; - gap: 12px; - margin: 4px 0 16px; - color: var(--accent-dim); - font-size: 0.6rem; - user-select: none; -} -.scene-break::before, .scene-break::after { - content: ''; - height: 1px; - flex: 1; - background: linear-gradient(90deg, transparent, rgba(150, 119, 58, 0.4), transparent); -} - -/* Thinking indicator — three motes pulsing in sequence. */ -.thinking { - display: inline-flex; - align-items: center; - gap: 10px; - font-family: var(--font-ui); - font-size: 0.78rem; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--accent-dim); -} -.thinking i { - width: 5px; - height: 5px; - border-radius: 50%; - background: var(--accent); - box-shadow: 0 0 8px var(--accent-glow); - animation: mote 1.25s ease-in-out infinite; -} -.thinking i:nth-child(2) { animation-delay: 0.18s; } -.thinking i:nth-child(3) { animation-delay: 0.36s; } -@keyframes mote { - 0%, 100% { opacity: 0.25; transform: scale(0.8); } - 45% { opacity: 1; transform: scale(1.15); } -} - -/* ---------- Scenario art picker (editor) ---------- */ - -.art-picker { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; } -.art-preview { - width: 76px; - height: 76px; - flex: none; - border-radius: 3px; - border: 1px solid var(--accent-dim); - box-shadow: inset 0 0 0 1px rgba(10, 10, 15, 0.7); - overflow: hidden; - display: grid; - place-items: center; -} -.art-preview img { width: 100%; height: 100%; object-fit: cover; display: block; } -.art-preview .glyph { font-size: 2rem; line-height: 1; } -.art-preview .mono { - font-family: var(--font-display); - font-size: 1.4rem; - font-weight: 700; - color: rgba(232, 196, 118, 0.92); -} -.art-controls { flex: 1; min-width: 220px; display: flex; flex-direction: column; gap: 9px; } -.art-buttons { display: flex; gap: 8px; flex-wrap: wrap; } -.icon-row { display: flex; gap: 5px; flex-wrap: wrap; } -.icon-row button { - padding: 3px 8px; - font-size: 1rem; - line-height: 1.2; - min-width: 34px; -} -.icon-row button.active { border-color: var(--accent); box-shadow: 0 0 10px var(--accent-glow); } -.art-hint { font-size: 0.76rem; color: var(--text-dim); line-height: 1.45; margin: 0; } - -/* ---------- AI Chat (power users) ---------- - A plain chat scratchpad, styled as a transcript rather than as the story - page: UI font, role labels, and a sticky composer at the bottom. */ - -.chat-page { - max-width: 820px; - display: flex; - flex-direction: column; - margin-bottom: var(--kb-inset, 0px); /* see .play-page */ -} -.chat-meta { - font-size: 0.78rem; - margin: -10px 0 16px; - word-break: break-word; -} -.chat-options { - border: 1px solid var(--border); - border-radius: 10px; - padding: 14px 16px 4px; - margin-bottom: 18px; - background: var(--bg-panel); -} - -.chat-transcript { flex: 1; display: flex; flex-direction: column; gap: 16px; padding-bottom: 20px; } - -.chat-msg { - border: 1px solid var(--border); - border-radius: 10px; - padding: 10px 14px 12px; - background: var(--bg-panel); -} -.chat-msg.user { border-left: 3px solid var(--player); background: var(--bg-input); } -.chat-msg.assistant { border-left: 3px solid var(--accent-dim); } -.chat-msg-head { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 6px; - font-size: 0.72rem; - letter-spacing: 0.14em; - text-transform: uppercase; -} -.chat-msg .chat-role { color: var(--accent-dim); font-weight: 600; } -.chat-msg.user .chat-role { color: var(--player); } -.chat-model-tag { text-transform: none; letter-spacing: 0; font-size: 0.74rem; } -.chat-msg-actions { - margin-left: auto; - display: flex; - gap: 12px; - opacity: 0; - transition: opacity 0.15s; -} -.chat-msg:hover .chat-msg-actions, .chat-msg:focus-within .chat-msg-actions { opacity: 1; } -.chat-msg-body { - white-space: pre-wrap; - overflow-wrap: anywhere; - line-height: 1.6; - font-size: 0.95rem; -} -.chat-msg .cursor { - color: var(--accent-bright); - text-shadow: 0 0 8px var(--accent-glow); - animation: blink 1s steps(1) infinite; -} - -/* Same collapsed thinking block as the story page, re-scoped for the transcript. */ -.chat-msg .reasoning { margin: 0 0 8px; font-size: 0.82rem; line-height: 1.5; color: var(--text-dim); } -.chat-msg .reasoning summary { cursor: pointer; user-select: none; opacity: 0.8; } -.chat-msg .reasoning summary:hover { opacity: 1; } -.chat-msg .reasoning .reasoning-text { - margin-top: 6px; - padding: 8px 12px; - border-left: 2px solid var(--border); - white-space: pre-wrap; - opacity: 0.85; - max-height: 260px; - overflow-y: auto; -} - -.chat-composer { - position: sticky; - bottom: var(--kb-inset, 0px); /* clears the on-screen keyboard — see .play-controls */ - display: flex; - gap: 10px; - align-items: flex-end; - padding: 14px 0; - background: linear-gradient(to top, var(--bg) 55%, transparent); -} -.chat-composer textarea { - flex: 1; - min-height: 44px; - max-height: 220px; - resize: none; - overflow-y: auto; -} -.chat-composer button { flex: none; height: 40px; } - -/* ---------- The story tree, drawn (branch map overlay) ---------- */ - -.branch-map-open { - align-self: flex-start; - padding: 5px 12px; - font-size: 0.74rem; - letter-spacing: 0.08em; - color: var(--accent); - background: transparent; - border: 1px solid var(--border-bright); -} -.branch-map-open:hover { color: var(--accent-bright); border-color: var(--accent-dim); } - -/* The overlay is deliberately wider and shorter than .modal: a tree is read - across, and the number of branches decides the height. */ -.branch-map-overlay { z-index: 60; } -.branch-map { - display: flex; - flex-direction: column; - width: min(1080px, calc(100vw - 40px)); - max-height: min(760px, calc(100vh - 60px)); - background: var(--bg-panel); - border: 1px solid var(--border-bright); - border-radius: 14px; - box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6), 0 0 40px rgba(212, 169, 78, 0.06); -} -.branch-map-header { - display: flex; - align-items: baseline; - gap: 12px; - padding: 18px 22px 12px; - border-bottom: 1px solid var(--border); -} -.branch-map-header h2 { - margin: 0; - font-family: var(--font-display); - font-size: 1.15rem; - color: var(--accent-bright); -} -.branch-map-scale { - font-size: 0.72rem; - color: var(--text-dim); - font-variant-numeric: tabular-nums; -} -.branch-map-close { - margin-left: auto; - align-self: center; - padding: 2px 8px; - color: var(--text-dim); - background: transparent; - border: 1px solid transparent; -} -.branch-map-close:hover { color: var(--accent-bright); border-color: var(--border); } - -.branch-map-canvas { - flex: 1; - min-height: 120px; - /* Height comes from the branch count, so a deep tree scrolls rather than the - diagram shrinking until nobody can read it. Width never scrolls — the map - is scaled to fit, which is the whole point of drawing it to one clock. */ - overflow-y: auto; - overflow-x: hidden; - padding: 4px 12px 8px; -} -.branch-map-svg { display: block; } - -.bm-tick line { stroke: var(--border); stroke-width: 1; stroke-dasharray: 2 6; } -.bm-tick text { - fill: var(--text-dim); - font-family: var(--font-ui); - font-size: 10px; - font-variant-numeric: tabular-nums; -} - -.bm-lane { cursor: pointer; } -.bm-hit { fill: transparent; } -.bm-lane:hover .bm-hit { fill: rgba(212, 169, 78, 0.035); } -.bm-lane.picked .bm-hit { fill: rgba(212, 169, 78, 0.07); } -.bm-lane:focus { outline: none; } -.bm-lane:focus-visible .bm-hit { fill: rgba(212, 169, 78, 0.12); } - -.bm-line { - stroke: var(--border-bright); - stroke-width: 3; - stroke-linecap: round; -} -.bm-fork { - fill: none; - stroke: var(--border-bright); - stroke-width: 2; -} -.bm-fork-dot, .bm-tip { fill: var(--border-bright); } - -.bm-lane:hover .bm-line, .bm-lane.picked .bm-line { stroke: var(--accent-dim); } -.bm-lane:hover .bm-fork, .bm-lane.picked .bm-fork { stroke: var(--accent-dim); } -.bm-lane:hover .bm-tip, .bm-lane.picked .bm-tip { fill: var(--accent); } - -/* The line being read: brighter, and its tip carries the glow so where you - are standing is findable before a word is read. */ -.bm-lane.here .bm-line { stroke: var(--accent); } -.bm-lane.here .bm-fork { stroke: var(--accent-dim); } -.bm-lane.here .bm-tip { - fill: var(--accent-bright); - filter: drop-shadow(0 0 6px var(--accent-glow)); -} -.bm-lane.here .bm-name { fill: var(--accent-bright); } - -.bm-name { - fill: var(--text); - font-family: var(--font-story); - font-size: 15px; -} -.bm-meta { - fill: var(--text-dim); - font-family: var(--font-ui); - font-size: 10.5px; - font-variant-numeric: tabular-nums; -} - -.branch-map-hint { - margin: 0; - padding: 0 22px 10px; - color: var(--text-dim); - font-size: 0.82rem; - line-height: 1.55; -} - -.branch-map-detail { - display: flex; - flex-direction: column; - gap: 6px; - padding: 14px 22px 18px; - border-top: 1px solid var(--border); -} -.branch-map-detail.here { background: rgba(212, 169, 78, 0.05); } -.bmd-head { display: flex; align-items: center; gap: 8px; } -.branch-map-detail .branch-name { font-size: 1.05rem; } -.branch-map-detail.here .branch-name { color: var(--accent-bright); } -.branch-map-detail .branch-tools button, .branch-map-detail .branch-confirm button { - padding: 4px 11px; - font-size: 0.74rem; - color: var(--text-dim); - background: transparent; - border: 1px solid var(--border); -} -.branch-map-detail .branch-tools button:hover:not(:disabled), -.branch-map-detail .branch-confirm button:hover:not(:disabled) { - color: var(--accent-bright); - border-color: var(--border-bright); -} - -@media (prefers-reduced-motion: reduce) { - .enter, .skeleton-card, .sk, .toast, .thinking i, - .story .action, .page { - animation: none !important; - } - .card.tome:hover { transform: none; } -} - -/* ---------- Visit analytics (owner dashboard) ---------- */ - -/* Wider than the reading pages: this one is a grid of small figures, not - prose, and 960px puts two charts and eight tiles into a column. */ -.an-page { max-width: 1180px; } -.an-ranges { display: flex; flex-wrap: wrap; gap: 6px; } - -/* Four across, not auto-fit: there are eight tiles, and letting them flow - leaves a single orphan on the second row at most widths. */ -.an-tiles { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; - margin-bottom: 18px; -} -@media (max-width: 900px) { - .an-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } -} -.an-tile { - background: var(--bg-panel); - border: 1px solid var(--border); - border-radius: 10px; - padding: 14px 16px; -} -.an-tile-value { - font-family: var(--font-display); - font-size: 1.7rem; - line-height: 1.1; - color: var(--accent-bright); -} -.an-tile-label { font-size: 0.82rem; color: var(--text); margin-top: 4px; } -.an-tile-hint { font-size: 0.72rem; color: var(--text-dim); margin-top: 2px; } - -.an-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); - gap: 16px; - margin-bottom: 16px; -} -.an-card { - position: relative; - background: var(--bg-panel); - border: 1px solid var(--border); - border-radius: 12px; - padding: 16px 18px 14px; - margin-bottom: 16px; -} -.an-grid .an-card { margin-bottom: 0; } -.an-card-head { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 12px; - margin-bottom: 14px; -} -.an-card-head h2 { - margin: 0; - font-family: var(--font-display); - font-size: 0.95rem; - letter-spacing: 0.06em; - color: var(--accent); -} -.an-note { font-size: 0.72rem; color: var(--text-dim); } -.an-empty { color: var(--text-dim); font-style: italic; padding: 18px 0; text-align: center; } - -.an-legend { display: flex; gap: 12px; font-size: 0.72rem; color: var(--text-dim); } -.an-legend-item { display: inline-flex; align-items: center; gap: 5px; } -.an-swatch { width: 9px; height: 9px; border-radius: 2px; display: inline-block; flex: none; } - -/* ----- Day columns ----- */ - -.an-plot { position: relative; height: 170px; margin-top: 12px; } -.an-gridline { - position: absolute; - left: 0; - right: 0; - border-top: 1px dashed var(--border); - pointer-events: none; -} -.an-gridline span { - position: absolute; - right: 0; - top: -0.62em; - font-size: 0.68rem; - color: var(--text-dim); - background: var(--bg-panel); - padding: 0 4px; - /* The dashed rule belongs behind the bars; its value does not. Columns are - positioned, so without this the tallest bar swallows the label. */ - z-index: 3; -} -.an-columns { display: flex; align-items: flex-end; gap: 2px; height: 100%; } -.an-column { - position: relative; - flex: 1 1 0; - min-width: 0; - height: 100%; - display: flex; - align-items: flex-end; - border-radius: 4px 4px 0 0; - outline: none; -} -.an-column.hot { background: rgba(212, 169, 78, 0.07); } -.an-stack { - width: 100%; - /* Capped so a 7-day range draws bars and not slabs; the column around it - stays full width, so the hover target does not shrink with the mark. */ - max-width: 44px; - margin: 0 auto; - height: 100%; - display: flex; - flex-direction: column; - justify-content: flex-end; - /* A 2px gap of surface between stacked segments: the boundary reads as a - boundary without a border, which would eat a thin bar entirely. */ - gap: 2px; -} -.an-bar { width: 100%; transition: filter 0.15s; } -.an-stack > .an-bar:first-child { border-radius: 4px 4px 0 0; } -.an-column.hot .an-bar { filter: brightness(1.2); } -.an-tip { - position: absolute; - bottom: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); - display: flex; - flex-direction: column; - gap: 3px; - padding: 8px 10px; - background: var(--bg-panel); - border: 1px solid var(--border-bright); - border-radius: 8px; - box-shadow: 0 10px 26px rgba(0, 0, 0, 0.55); - font-size: 0.74rem; - white-space: nowrap; - pointer-events: none; - z-index: 4; -} -.an-tip strong { font-weight: 600; color: var(--accent-bright); } -.an-tip span { display: inline-flex; align-items: center; gap: 6px; color: var(--text-dim); } - -.an-axis { display: flex; gap: 2px; margin-top: 7px; } -.an-axis span { - flex: 1 1 0; - min-width: 0; - font-size: 0.68rem; - color: var(--text-dim); - text-align: center; - white-space: nowrap; -} -.an-axis span:first-child { text-align: left; } -.an-axis span:last-child { text-align: right; } - -.an-overlay-empty { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - color: var(--text-dim); - font-style: italic; - font-size: 0.85rem; - pointer-events: none; -} - -/* ----- Funnel ----- */ - -.an-funnel { display: flex; flex-direction: column; gap: 10px; } -.an-funnel-row { - display: grid; - grid-template-columns: 168px 1fr 96px; - align-items: center; - gap: 12px; -} -.an-funnel-label { font-size: 0.84rem; } -.an-funnel-track { height: 14px; background: var(--bg-input); border-radius: 7px; overflow: hidden; } -.an-funnel-bar { - height: 100%; - min-width: 2px; - background: var(--chart-1); - border-radius: 7px; - transition: width 0.4s ease; -} -.an-funnel-value { - text-align: right; - font-size: 0.86rem; - display: flex; - justify-content: flex-end; - align-items: baseline; - gap: 8px; -} -.an-funnel-share { font-size: 0.72rem; color: var(--text-dim); } - -/* ----- Ranked lists ----- */ - -.an-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; } -.an-list li { - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 7px 10px; - border-radius: 6px; - font-size: 0.84rem; -} -/* The bar sits behind the row rather than beside it, so a long label keeps the - full width and the magnitude is still visible at a glance. */ -.an-list-fill { - position: absolute; - top: 0; - bottom: 0; - left: 0; - background: rgba(181, 138, 48, 0.2); - border-radius: 6px; -} -.an-list-label { - position: relative; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.an-list-value { position: relative; color: var(--text-dim); flex: none; } - -/* ----- Tabs, and the access log table ----- */ - -.an-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid var(--border); } -.an-tabs button { - background: none; - border: none; - border-bottom: 2px solid transparent; - border-radius: 0; - padding: 8px 14px; - color: var(--text-dim); - font-family: var(--font-display); - font-size: 0.82rem; - letter-spacing: 0.06em; - cursor: pointer; -} -.an-tabs button:hover { color: var(--text); } -.an-tabs button.active { color: var(--accent-bright); border-bottom-color: var(--accent); } - -.an-log-head { flex-wrap: wrap; row-gap: 10px; } -.an-log-search { max-width: 260px; } - -/* The table is the one thing here with a minimum width — six columns of real - data don't compress. It scrolls inside its own box so the page never does. */ -.an-table-scroll { overflow-x: auto; } -.an-table { width: 100%; min-width: 620px; border-collapse: collapse; font-size: 0.82rem; } -.an-table th { - text-align: left; - font-weight: 500; - font-size: 0.72rem; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--text-dim); - padding: 0 10px 8px; - border-bottom: 1px solid var(--border); - white-space: nowrap; -} -.an-table td { padding: 8px 10px; border-bottom: 1px solid var(--border); white-space: nowrap; } -.an-table tr:last-child td { border-bottom: none; } -.an-table tbody tr:hover { background: rgba(212, 169, 78, 0.05); } -/* A failed attempt is the row you are scanning for; it gets the only colour in - the table, on a border rather than the text, which stays readable. */ -.an-table tr.failed td:first-child { box-shadow: inset 2px 0 var(--danger); } -.an-table tr.failed td:nth-child(3) { color: var(--danger); } -.an-cell-dim { color: var(--text-dim); } -.an-cell-mono { font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; } -.an-tag { - margin-left: 8px; - padding: 1px 6px; - border: 1px solid var(--border-bright); - border-radius: 10px; - font-size: 0.66rem; - color: var(--text-dim); -} -.an-more { display: flex; justify-content: center; padding-top: 14px; } - -.an-footnote { - color: var(--text-dim); - font-size: 0.74rem; - line-height: 1.6; - margin: 22px 0 0; - max-width: 70ch; -} - -/* ============================================================ - Mobile / narrow screens (≤ 720px) - The desktop Play screen is a horizontal row of up to four columns - (two left rails + story + right side-panel). Phones can't fit that, - so here we collapse to a single story column and turn the rails and - side-panel into full-height overlays. Everything above this query is - the untouched desktop design. - ============================================================ */ -@media (max-width: 720px) { - - /* ---------- Top nav → hamburger dropdown ---------- */ - .topnav { gap: 12px; padding: 0 16px; } - .nav-hamburger { display: block; } - .nav-links { - display: none; - position: absolute; - top: 56px; - left: 0; - right: 0; - flex-direction: column; - align-items: stretch; - background: var(--bg-panel); - border-bottom: 1px solid var(--border); - box-shadow: 0 14px 34px rgba(0, 0, 0, 0.55); - padding: 4px 0; - } - .nav-links.open { display: flex; } - .topnav a.navlink { - padding: 13px 20px; - border-bottom: none; - border-left: 3px solid transparent; - } - .topnav a.navlink.active { - border-bottom-color: transparent; - border-left-color: var(--accent); - } - .nav-account { - margin-left: 0; - padding: 12px 20px; - border-top: 1px solid var(--border); - flex-wrap: wrap; - } - .account-email { max-width: 100%; } - - /* ---------- Global page padding / typography ---------- */ - .page { padding: 18px 14px; } - /* 16px inputs stop iOS Safari auto-zooming when a field is focused. */ - input[type="text"], input[type="email"], input[type="password"], input[type="number"], textarea, select { - font-size: 16px; - } - - /* ---------- Play screen → single column ---------- */ - .play-layout { flex-direction: column; } - .play-layout .play-page, - .play-layout.with-panel .play-page { max-width: 100%; } - .play-page .page-header h1 { font-size: 1.25rem; } - .panel-toggles { flex-wrap: wrap; } - .story { font-size: 1.06rem; } - /* Reserve a left gutter so the fixed World/State edge-tabs never sit on - top of the story text as it scrolls past them. */ - .story, .play-page .empty { padding-left: 26px; } - .story .action { padding-right: 0; } - /* No hover on touch — keep per-action edit/delete tools visible. */ - .story .action-tools { opacity: 1; pointer-events: auto; } - - /* ---------- Left rails (World State / Script State) → edge tabs + overlay ---------- */ - /* Closed: a compact tab pinned to the left edge instead of a full-height - column that steals the story's width. World State sits above Script State. - (World State only renders for RPG adventures, so it may be absent.) */ - .status-drawer { - position: fixed; - left: 0; - z-index: 40; - } - .status-drawer:not(.open) { - top: 170px; - height: auto; - background: var(--bg-panel); - border: 1px solid var(--border); - border-left: none; - border-radius: 0 10px 10px 0; - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); - } - .ws-drawer:not(.open) { top: 170px; } - .status-drawer:not(.ws-drawer):not(.open) { top: 260px; } - .status-drawer:not(.open) .status-toggle { height: auto; padding: 14px 4px; } - - /* Open: slide-in overlay covering most of the screen. */ - .status-drawer.open { - top: 56px; - height: calc(100dvh - 56px); - width: min(88vw, 320px); - box-shadow: 0 0 50px rgba(0, 0, 0, 0.65); - border-right: 1px solid var(--border-bright); - } - - /* ---------- Right side-panel (Plot/Memory/Scripts/Insights) → full overlay ---------- */ - .side-panel { - position: fixed; - inset: 56px 0 0 0; - width: auto; - height: auto; - z-index: 45; - border-left: none; - padding: 16px; - } - - /* ---------- Home / library surfaces ---------- */ - - /* Both grids go single-column: the wide variant's 320px minimum would - otherwise overflow a 360px phone once card padding is counted. */ - .card-grid, .card-grid.wide { grid-template-columns: 1fr; } - - .hall { padding: 2px 0 20px; } - .hall-sub { font-size: 0.95rem; } - .home-section { margin-top: 26px; } - - /* The label eats most of a narrow line, so drop the short leading rule and - let the trailing one take whatever space is left. */ - .rule { letter-spacing: 0.16em; font-size: 0.76rem; gap: 10px; } - .rule::before { display: none; } - .rule-head { gap: 10px; } - - /* Ornament scales down rather than crowding the corners on a small card. */ - .card.tome { padding: 14px 15px 12px; } - .card.tome::before, .card.tome::after { width: 9px; height: 9px; } - /* Always-on filigree: there's no hover on touch. */ - .card.tome::before, .card.tome::after { opacity: 0.8; } - .seal { width: 21px; height: 21px; top: 10px; right: 10px; font-size: 0.54rem; } - .card.tome:has(> .seal) .card-head { padding-right: 22px; } - .snippet { font-size: 0.94rem; -webkit-line-clamp: 4; } - .snippet.dropcap::first-letter { font-size: 2rem; padding-right: 6px; } - - /* Motes are pure decoration — thin them out where the GPU is weakest and - the effect is least visible. (Embers.jsx already scales count to the - viewport; this drops the opacity too.) */ - .embers { opacity: 0.55; } - - /* Full-width toasts, clear of the sticky play controls. */ - .toast-host { - left: 12px; - right: 12px; - bottom: 14px; - transform: none; - width: auto; - max-width: none; - align-items: stretch; - } - .toast { font-size: 0.82rem; } - - /* Art picker stacks; the emoji grid stays tappable at 34px minimum. */ - .art-picker { gap: 12px; } - .art-preview { width: 64px; height: 64px; } - .art-controls { min-width: 0; } - - /* The drop cap on the opening beat competes with the edge tabs at this - width, so shrink it and keep it inside the 26px story inset. */ - .story .action.opening::first-letter { - font-size: 2.5rem; - padding: 4px 8px 0 0; - } - - /* ---------- Composers: give the typing area the full width ---------- - Both composers are one flex row on desktop, which on a phone left the - textarea with a sliver: the Play bar spends ~180px on Do/Say/Story plus - ~75px on Send, and the chat bar ~90px on Send. Letting the row wrap and - moving the buttons onto their own line hands that width back to the - textarea, which is the part you actually type into. - - Play: Do/Say/Story and Send share the top row (Send pushed to the right - edge), and `order` drops the textarea onto its own full-width row beneath - them. Two rows, and the typing area goes from ~138px to the full width — - measured at a 390px composer. */ - .input-bar { flex-wrap: wrap; } - .mode-select button { flex: 1; padding: 7px 10px; } - .input-bar > button { margin-left: auto; } - /* min-width:0 lets a flex item shrink below its content width. */ - .input-bar textarea { order: 1; flex: 1 0 100%; margin-top: 8px; min-width: 0; } - - /* ---------- AI Chat ---------- */ - /* No hover on touch, so the per-message copy/delete links stay visible. */ - .chat-msg-actions { opacity: 1; } - .chat-msg { padding: 9px 11px 11px; } - .chat-composer { padding: 10px 0 14px; flex-wrap: wrap; } - /* Full-width textarea; Send wraps beneath it and sits at the right edge. */ - .chat-composer textarea { flex: 1 1 100%; max-height: 140px; } - .chat-composer button { margin-left: auto; } - - /* ---------- The branch map ---------- */ - /* A phone has no room for a dialog inset from the edges, and the tree is the - one thing on this screen that wants every pixel across. */ - .branch-map { - width: 100vw; - max-width: none; - max-height: 100dvh; - height: 100dvh; - border: none; - border-radius: 0; - } - .branch-map-header { padding: 14px 16px 10px; flex-wrap: wrap; } - .branch-map-canvas { padding: 4px 6px 8px; } - .branch-map-detail { padding: 12px 16px 16px; } - .branch-map-hint { padding: 0 16px 10px; } - - /* ---------- Analytics dashboard ---------- */ - .an-page .page-header { flex-direction: column; align-items: flex-start; gap: 10px; } - .an-tiles { gap: 8px; } - .an-tile { padding: 10px 12px; } - .an-tile-value { font-size: 1.35rem; } - .an-grid { grid-template-columns: 1fr; } - .an-plot { height: 140px; } - .an-tabs button { padding: 8px 10px; font-size: 0.78rem; } - .an-log-search { max-width: none; width: 100%; } - /* Three columns don't fit; the label goes above its own bar instead. */ - .an-funnel-row { grid-template-columns: 1fr auto; gap: 4px 10px; } - .an-funnel-track { grid-column: 1; } - .an-funnel-label { grid-column: 1 / -1; } - .an-funnel-value { grid-column: 2; } -} +/* The stylesheet, split into sections and imported in order. + + The order is load-bearing and must not change. Several selectors in + `tome.css` tie with earlier ones on specificity and win only because they + come later, and `responsive.css` overrides the whole desktop design at + narrow widths. Both are noted where the rules are. + + Add a new section by adding a file and an `@import` for it. Put the import + where the rules belong in the cascade, not at the end by habit. +*/ +@import './styles/tokens.css'; /* custom properties: colors, fonts, spacing */ +@import './styles/base.css'; /* scrollbars */ +@import './styles/nav.css'; /* the top navigation bar */ +@import './styles/forms.css'; /* inputs, labels, and buttons */ +@import './styles/cards.css'; /* cards, lists, and the story-card editor */ +@import './styles/play.css'; /* the Play screen and the branches panel */ +@import './styles/panels.css'; /* the side panel and the in-play script viewer */ +@import './styles/drawers.css'; /* the status drawer and the world-state drawer */ +@import './styles/schema-editor.css'; /* the stat-schema form and the NPC roster */ +@import './styles/insights.css'; /* insights, scripts, and the memory bank */ +@import './styles/modals.css'; /* the filter bar, tags, and modals */ +@import './styles/auth.css'; /* log in, sign up, and the settings debug log */ +@import './styles/banners.css'; /* the Play screen's persistent banner */ +@import './styles/tome.css'; /* the illuminated-tome surfaces */ +@import './styles/chat.css'; /* the AI Chat scratchpad */ +@import './styles/tree-map.css'; /* the branch map overlay */ +@import './styles/analytics.css'; /* the visitor dashboard */ +@import './styles/responsive.css'; /* narrow screens, 720px and below */ diff --git a/frontend/src/styles/analytics.css b/frontend/src/styles/analytics.css new file mode 100644 index 0000000..b56cdbf --- /dev/null +++ b/frontend/src/styles/analytics.css @@ -0,0 +1,287 @@ +/* ---------- Visit analytics (owner dashboard) ---------- */ + +/* Wider than the reading pages: this one is a grid of small figures, not + prose, and 960px puts two charts and eight tiles into a column. */ +.an-page { max-width: 1180px; } +.an-ranges { display: flex; flex-wrap: wrap; gap: 6px; } + +/* Four across, not auto-fit: there are eight tiles, and letting them flow + leaves a single orphan on the second row at most widths. */ +.an-tiles { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 18px; +} +@media (max-width: 900px) { + .an-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} +.an-tile { + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: 10px; + padding: 14px 16px; +} +.an-tile-value { + font-family: var(--font-display); + font-size: 1.7rem; + line-height: 1.1; + color: var(--accent-bright); +} +.an-tile-label { font-size: 0.82rem; color: var(--text); margin-top: 4px; } +.an-tile-hint { font-size: 0.72rem; color: var(--text-dim); margin-top: 2px; } + +.an-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); + gap: 16px; + margin-bottom: 16px; +} +.an-card { + position: relative; + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: 12px; + padding: 16px 18px 14px; + margin-bottom: 16px; +} +.an-grid .an-card { margin-bottom: 0; } +.an-card-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; +} +.an-card-head h2 { + margin: 0; + font-family: var(--font-display); + font-size: 0.95rem; + letter-spacing: 0.06em; + color: var(--accent); +} +.an-note { font-size: 0.72rem; color: var(--text-dim); } +.an-empty { color: var(--text-dim); font-style: italic; padding: 18px 0; text-align: center; } + +.an-legend { display: flex; gap: 12px; font-size: 0.72rem; color: var(--text-dim); } +.an-legend-item { display: inline-flex; align-items: center; gap: 5px; } +.an-swatch { width: 9px; height: 9px; border-radius: 2px; display: inline-block; flex: none; } + +/* ----- Day columns ----- */ + +.an-plot { position: relative; height: 170px; margin-top: 12px; } +.an-gridline { + position: absolute; + left: 0; + right: 0; + border-top: 1px dashed var(--border); + pointer-events: none; +} +.an-gridline span { + position: absolute; + right: 0; + top: -0.62em; + font-size: 0.68rem; + color: var(--text-dim); + background: var(--bg-panel); + padding: 0 4px; + /* The dashed rule belongs behind the bars; its value does not. Columns are + positioned, so without this the tallest bar swallows the label. */ + z-index: 3; +} +.an-columns { display: flex; align-items: flex-end; gap: 2px; height: 100%; } +.an-column { + position: relative; + flex: 1 1 0; + min-width: 0; + height: 100%; + display: flex; + align-items: flex-end; + border-radius: 4px 4px 0 0; + outline: none; +} +.an-column.hot { background: rgba(212, 169, 78, 0.07); } +.an-stack { + width: 100%; + /* Capped so a 7-day range draws bars and not slabs; the column around it + stays full width, so the hover target does not shrink with the mark. */ + max-width: 44px; + margin: 0 auto; + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; + /* A 2px gap of surface between stacked segments: the boundary reads as a + boundary without a border, which would eat a thin bar entirely. */ + gap: 2px; +} +.an-bar { width: 100%; transition: filter 0.15s; } +.an-stack > .an-bar:first-child { border-radius: 4px 4px 0 0; } +.an-column.hot .an-bar { filter: brightness(1.2); } +.an-tip { + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + gap: 3px; + padding: 8px 10px; + background: var(--bg-panel); + border: 1px solid var(--border-bright); + border-radius: 8px; + box-shadow: 0 10px 26px rgba(0, 0, 0, 0.55); + font-size: 0.74rem; + white-space: nowrap; + pointer-events: none; + z-index: 4; +} +.an-tip strong { font-weight: 600; color: var(--accent-bright); } +.an-tip span { display: inline-flex; align-items: center; gap: 6px; color: var(--text-dim); } + +.an-axis { display: flex; gap: 2px; margin-top: 7px; } +.an-axis span { + flex: 1 1 0; + min-width: 0; + font-size: 0.68rem; + color: var(--text-dim); + text-align: center; + white-space: nowrap; +} +.an-axis span:first-child { text-align: left; } +.an-axis span:last-child { text-align: right; } + +.an-overlay-empty { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-dim); + font-style: italic; + font-size: 0.85rem; + pointer-events: none; +} + +/* ----- Funnel ----- */ + +.an-funnel { display: flex; flex-direction: column; gap: 10px; } +.an-funnel-row { + display: grid; + grid-template-columns: 168px 1fr 96px; + align-items: center; + gap: 12px; +} +.an-funnel-label { font-size: 0.84rem; } +.an-funnel-track { height: 14px; background: var(--bg-input); border-radius: 7px; overflow: hidden; } +.an-funnel-bar { + height: 100%; + min-width: 2px; + background: var(--chart-1); + border-radius: 7px; + transition: width 0.4s ease; +} +.an-funnel-value { + text-align: right; + font-size: 0.86rem; + display: flex; + justify-content: flex-end; + align-items: baseline; + gap: 8px; +} +.an-funnel-share { font-size: 0.72rem; color: var(--text-dim); } + +/* ----- Ranked lists ----- */ + +.an-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; } +.an-list li { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 7px 10px; + border-radius: 6px; + font-size: 0.84rem; +} +/* The bar sits behind the row rather than beside it, so a long label keeps the + full width and the magnitude is still visible at a glance. */ +.an-list-fill { + position: absolute; + top: 0; + bottom: 0; + left: 0; + background: rgba(181, 138, 48, 0.2); + border-radius: 6px; +} +.an-list-label { + position: relative; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.an-list-value { position: relative; color: var(--text-dim); flex: none; } + +/* ----- Tabs, and the access log table ----- */ + +.an-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid var(--border); } +.an-tabs button { + background: none; + border: none; + border-bottom: 2px solid transparent; + border-radius: 0; + padding: 8px 14px; + color: var(--text-dim); + font-family: var(--font-display); + font-size: 0.82rem; + letter-spacing: 0.06em; + cursor: pointer; +} +.an-tabs button:hover { color: var(--text); } +.an-tabs button.active { color: var(--accent-bright); border-bottom-color: var(--accent); } + +.an-log-head { flex-wrap: wrap; row-gap: 10px; } +.an-log-search { max-width: 260px; } + +/* The table is the one thing here with a minimum width — six columns of real + data don't compress. It scrolls inside its own box so the page never does. */ +.an-table-scroll { overflow-x: auto; } +.an-table { width: 100%; min-width: 620px; border-collapse: collapse; font-size: 0.82rem; } +.an-table th { + text-align: left; + font-weight: 500; + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-dim); + padding: 0 10px 8px; + border-bottom: 1px solid var(--border); + white-space: nowrap; +} +.an-table td { padding: 8px 10px; border-bottom: 1px solid var(--border); white-space: nowrap; } +.an-table tr:last-child td { border-bottom: none; } +.an-table tbody tr:hover { background: rgba(212, 169, 78, 0.05); } +/* A failed attempt is the row you are scanning for; it gets the only colour in + the table, on a border rather than the text, which stays readable. */ +.an-table tr.failed td:first-child { box-shadow: inset 2px 0 var(--danger); } +.an-table tr.failed td:nth-child(3) { color: var(--danger); } +.an-cell-dim { color: var(--text-dim); } +.an-cell-mono { font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; } +.an-tag { + margin-left: 8px; + padding: 1px 6px; + border: 1px solid var(--border-bright); + border-radius: 10px; + font-size: 0.66rem; + color: var(--text-dim); +} +.an-more { display: flex; justify-content: center; padding-top: 14px; } + +.an-footnote { + color: var(--text-dim); + font-size: 0.74rem; + line-height: 1.6; + margin: 22px 0 0; + max-width: 70ch; +} + diff --git a/frontend/src/styles/auth.css b/frontend/src/styles/auth.css new file mode 100644 index 0000000..8e7b017 --- /dev/null +++ b/frontend/src/styles/auth.css @@ -0,0 +1,145 @@ +/* ---------- Auth (log in / sign up) ---------- */ + +.auth-modal { width: min(400px, calc(100vw - 40px)); text-align: center; padding-top: 22px; } +.auth-crest { + font-size: 1.1rem; + color: var(--accent-dim); + letter-spacing: 0.3em; + margin-bottom: 6px; +} +.auth-modal h2 { font-size: 1.35rem; } +.auth-modal .auth-hint { margin-bottom: 16px; line-height: 1.45; } +/* Fields go back to left-aligned — only the header is centred. */ +.auth-modal label.field { text-align: left; } + +.auth-tabs { + display: flex; + gap: 4px; + padding: 4px; + margin-bottom: 18px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 10px; +} +.auth-tab { + flex: 1; + padding: 7px 0; + border: 1px solid transparent; + border-radius: 7px; + background: transparent; + color: var(--text-dim); + font-family: var(--font-display); + font-size: 0.78rem; + letter-spacing: 0.1em; + text-transform: uppercase; +} +.auth-tab:hover { color: var(--text); background: rgba(212, 169, 78, 0.06); } +.auth-tab.active { + background: var(--bg-panel); + border-color: var(--accent-dim); + color: var(--accent-bright); +} + +.auth-password { display: flex; gap: 8px; } +.auth-password input { flex: 1; min-width: 0; } +.auth-reveal { + flex: none; + padding: 0 12px; + font-size: 0.78rem; + color: var(--text-dim); +} +.auth-reveal:hover { color: var(--accent-bright); } + +.auth-help { + display: block; + margin-top: 6px; + font-size: 0.76rem; + color: var(--text-dim); +} +.auth-error { + margin-top: 4px; + padding: 9px 12px; + border: 1px solid rgba(208, 101, 101, 0.45); + border-radius: 8px; + background: rgba(208, 101, 101, 0.09); + color: var(--danger); + font-size: 0.85rem; + text-align: left; + white-space: pre-wrap; +} + +/* "Update from scenario" — a wider modal, since it lists a diff. */ +.refresh-modal { width: min(620px, calc(100vw - 40px)); } +.refresh-changes { + border: 1px solid var(--border); + border-radius: 10px; + padding: 12px 14px; + font-size: 0.86rem; +} +.refresh-change + .refresh-change { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--border); +} +.refresh-old, .refresh-new { margin-top: 3px; font-family: var(--font-mono, monospace); font-size: 0.8rem; } +.refresh-old { text-decoration: line-through; } +.refresh-new { color: var(--accent-bright); } +/* The button sits above the plot fields it would overwrite. */ +.plot-source { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); + font-size: 0.82rem; +} +.plot-source .linklike { flex-shrink: 0; } + +/* ---------- Debug log (Settings) ---------- */ + +.debug-entry { + border: 1px solid var(--border); + border-radius: 8px; + margin-bottom: 8px; + overflow: hidden; +} +.debug-entry-head { + display: flex; + align-items: baseline; + gap: 12px; + padding: 9px 12px; + cursor: pointer; + font-size: 0.86rem; + background: var(--bg-input); +} +.debug-entry-head .dim, .debug-entry-body .dim { color: var(--text-dim); font-size: 0.8rem; } +.debug-status { + font-size: 0.68rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 1px 8px; + border-radius: 999px; + border: 1px solid var(--border-bright); + color: var(--text-dim); +} +.debug-status.ok { color: #7dc9a2; border-color: #7dc9a244; } +.debug-status.error { color: var(--danger); border-color: var(--danger); } +.debug-status.pending { color: var(--accent); border-color: var(--accent-dim); } +.debug-entry-body { padding: 10px 12px; font-size: 0.85rem; } +.debug-entry-body pre { + margin: 2px 0 8px; + padding: 8px 10px; + background: var(--bg-input); + border-radius: 6px; + white-space: pre-wrap; + word-break: break-word; + font-family: Consolas, monospace; + font-size: 0.78rem; + max-height: 300px; + overflow-y: auto; +} + diff --git a/frontend/src/styles/banners.css b/frontend/src/styles/banners.css new file mode 100644 index 0000000..07658ea --- /dev/null +++ b/frontend/src/styles/banners.css @@ -0,0 +1,25 @@ +/* ---------- Play screen error/success banner ---------- + Persistent (no auto-dismiss) and can carry a Retry action, so it stays + separate from the transient global toasts further down this file. */ + +.play-toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: var(--bg-panel-glass); + backdrop-filter: blur(10px); + border: 1px solid var(--danger); + color: var(--text); + padding: 11px 20px; + border-radius: 10px; + z-index: 100; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5); + animation: toast-in 0.25s ease both; +} +.play-toast.ok { border-color: var(--accent); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5), 0 0 20px var(--accent-glow); } +@keyframes toast-in { + from { opacity: 0; transform: translate(-50%, 12px); } + to { opacity: 1; transform: translate(-50%, 0); } +} + diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css new file mode 100644 index 0000000..293e9c6 --- /dev/null +++ b/frontend/src/styles/base.css @@ -0,0 +1,11 @@ +/* ---------- Scrollbars ---------- */ + +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background: var(--border-bright); + border-radius: 5px; + border: 2px solid var(--bg); +} +::-webkit-scrollbar-thumb:hover { background: var(--accent-dim); } + diff --git a/frontend/src/styles/cards.css b/frontend/src/styles/cards.css new file mode 100644 index 0000000..01d96a8 --- /dev/null +++ b/frontend/src/styles/cards.css @@ -0,0 +1,63 @@ +/* ---------- Cards & lists ---------- */ + +.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; } +.card { + position: relative; + background: linear-gradient(170deg, #17172400, #17172400 60%, rgba(212, 169, 78, 0.03)), var(--bg-panel); + border: 1px solid var(--border); + border-radius: 12px; + padding: 18px; + cursor: pointer; + transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s; + overflow: hidden; +} +.card::before { + content: ''; + position: absolute; + inset: 0 0 auto 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--accent-dim), transparent); + opacity: 0; + transition: opacity 0.25s; +} +.card:hover { + border-color: var(--border-bright); + transform: translateY(-3px); + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45), 0 0 20px rgba(212, 169, 78, 0.06); +} +.card:hover::before { opacity: 1; } +.card h3 { + margin: 0 0 6px; + font-family: var(--font-display); + font-size: 1.02rem; + color: var(--accent); +} +.card p { margin: 0; color: var(--text-dim); font-size: 0.88rem; line-height: 1.5; } +.card .meta { margin-top: 12px; font-size: 0.75rem; color: var(--text-dim); } + +.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } +.page-header h1 { + margin: 0; + font-family: var(--font-display); + font-size: 1.55rem; + color: var(--accent-bright); + text-shadow: 0 0 24px var(--accent-glow); +} + +.empty { color: var(--text-dim); text-align: center; padding: 60px 0; font-style: italic; font-family: var(--font-story); font-size: 1.05rem; } + +/* ---------- Story card editor ---------- */ + +.storycard { + background: var(--bg-input); + border: 1px solid var(--border); + border-left: 3px solid var(--accent-dim); + border-radius: 8px; + padding: 12px; + margin-bottom: 10px; + transition: border-color 0.15s; +} +.storycard:focus-within { border-color: var(--accent-dim); } +.storycard .row { display: flex; gap: 10px; margin-bottom: 8px; } +.storycard .row > * { flex: 1; } + diff --git a/frontend/src/styles/chat.css b/frontend/src/styles/chat.css new file mode 100644 index 0000000..3cd4332 --- /dev/null +++ b/frontend/src/styles/chat.css @@ -0,0 +1,97 @@ +/* ---------- AI Chat (power users) ---------- + A plain chat scratchpad, styled as a transcript rather than as the story + page: UI font, role labels, and a sticky composer at the bottom. */ + +.chat-page { + max-width: 820px; + display: flex; + flex-direction: column; + margin-bottom: var(--kb-inset, 0px); /* see .play-page */ +} +.chat-meta { + font-size: 0.78rem; + margin: -10px 0 16px; + word-break: break-word; +} +.chat-options { + border: 1px solid var(--border); + border-radius: 10px; + padding: 14px 16px 4px; + margin-bottom: 18px; + background: var(--bg-panel); +} + +.chat-transcript { flex: 1; display: flex; flex-direction: column; gap: 16px; padding-bottom: 20px; } + +.chat-msg { + border: 1px solid var(--border); + border-radius: 10px; + padding: 10px 14px 12px; + background: var(--bg-panel); +} +.chat-msg.user { border-left: 3px solid var(--player); background: var(--bg-input); } +.chat-msg.assistant { border-left: 3px solid var(--accent-dim); } +.chat-msg-head { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 6px; + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; +} +.chat-msg .chat-role { color: var(--accent-dim); font-weight: 600; } +.chat-msg.user .chat-role { color: var(--player); } +.chat-model-tag { text-transform: none; letter-spacing: 0; font-size: 0.74rem; } +.chat-msg-actions { + margin-left: auto; + display: flex; + gap: 12px; + opacity: 0; + transition: opacity 0.15s; +} +.chat-msg:hover .chat-msg-actions, .chat-msg:focus-within .chat-msg-actions { opacity: 1; } +.chat-msg-body { + white-space: pre-wrap; + overflow-wrap: anywhere; + line-height: 1.6; + font-size: 0.95rem; +} +.chat-msg .cursor { + color: var(--accent-bright); + text-shadow: 0 0 8px var(--accent-glow); + animation: blink 1s steps(1) infinite; +} + +/* Same collapsed thinking block as the story page, re-scoped for the transcript. */ +.chat-msg .reasoning { margin: 0 0 8px; font-size: 0.82rem; line-height: 1.5; color: var(--text-dim); } +.chat-msg .reasoning summary { cursor: pointer; user-select: none; opacity: 0.8; } +.chat-msg .reasoning summary:hover { opacity: 1; } +.chat-msg .reasoning .reasoning-text { + margin-top: 6px; + padding: 8px 12px; + border-left: 2px solid var(--border); + white-space: pre-wrap; + opacity: 0.85; + max-height: 260px; + overflow-y: auto; +} + +.chat-composer { + position: sticky; + bottom: var(--kb-inset, 0px); /* clears the on-screen keyboard — see .play-controls */ + display: flex; + gap: 10px; + align-items: flex-end; + padding: 14px 0; + background: linear-gradient(to top, var(--bg) 55%, transparent); +} +.chat-composer textarea { + flex: 1; + min-height: 44px; + max-height: 220px; + resize: none; + overflow-y: auto; +} +.chat-composer button { flex: none; height: 40px; } + diff --git a/frontend/src/styles/drawers.css b/frontend/src/styles/drawers.css new file mode 100644 index 0000000..86d387a --- /dev/null +++ b/frontend/src/styles/drawers.css @@ -0,0 +1,266 @@ +/* ---------- Left status drawer (script state) ---------- */ + +.status-drawer { + flex-shrink: 0; + position: sticky; + top: 56px; + height: calc(100dvh - 56px); + display: flex; + align-items: flex-start; + border-right: 1px solid var(--border); + background: var(--bg-panel); +} +.status-drawer.open { width: 300px; } +.status-toggle { + flex-shrink: 0; + height: 100%; + width: 30px; + padding: 12px 0; + border: none; + border-radius: 0; + background: transparent; + color: var(--text-dim); + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} +.status-toggle:hover { color: var(--text); background: var(--bg-input); } +.status-toggle-label { + writing-mode: vertical-rl; + text-orientation: mixed; + letter-spacing: 0.12em; + font-size: 0.72rem; + text-transform: uppercase; +} +.status-body { + flex: 1; + min-width: 0; + height: 100%; + overflow-y: auto; + padding: 20px 16px; + animation: panel-in 0.2s ease both; +} +.status-vars { list-style: none; margin: 0; padding: 0; } +.status-vars li { + display: flex; + flex-direction: column; + gap: 2px; + padding: 8px 0; + border-bottom: 1px solid var(--border); +} +.status-key { + font-family: var(--font-mono, monospace); + font-size: 0.82rem; + color: var(--accent-bright); + word-break: break-word; +} +.status-val { + font-family: var(--font-mono, monospace); + font-size: 0.82rem; + color: var(--text); + white-space: pre-wrap; + word-break: break-word; +} +/* Recursive script-state value tree */ +.jv-string { color: var(--text); } +.jv-number { color: var(--player); } +.jv-bool { color: var(--accent-bright); } +.jv-null { color: var(--text-dim); font-style: italic; } +.jv-empty { color: var(--text-dim); } +.jv-tree { display: inline-block; vertical-align: top; max-width: 100%; } +.jv-toggle { + background: none; border: none; padding: 0; cursor: pointer; + font-family: inherit; font-size: 0.72rem; color: var(--text-dim); +} +.jv-toggle:hover { color: var(--accent-bright); } +.jv-children { + list-style: none; margin: 2px 0 2px 0; padding-left: 12px; + border-left: 1px solid var(--border); +} +.jv-children li { padding: 2px 0; word-break: break-word; } +.jv-key { color: var(--accent); margin-right: 6px; } +.jv-key::after { content: ':'; color: var(--text-dim); } +@keyframes panel-in { + from { opacity: 0; transform: translateX(16px); } + to { opacity: 1; transform: none; } +} + +/* ---------- World State drawer (Phase 12 RPG) ---------- */ +.ws-group { margin-bottom: 18px; } +.ws-group-title { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-dim); + margin: 0 0 8px; +} +.ws-stat { padding: 5px 0; } +.ws-stat-head { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 8px; + font-size: 0.85rem; +} +.ws-stat-name { color: var(--text); text-transform: capitalize; } +.ws-stat-val { + color: var(--text-dim); + font-variant-numeric: tabular-nums; + font-size: 0.8rem; + text-align: right; +} +.ws-band { color: var(--accent-bright); } +.ws-bar { + margin-top: 4px; + height: 5px; + border-radius: 3px; + background: var(--bg-input); + overflow: hidden; +} +.ws-bar-fill { + height: 100%; + border-radius: 3px; + background: var(--accent); + transition: width 0.35s ease; +} +/* Cast — each NPC is a small plate inside the one "Cast" group, so a roster of + five doesn't read as five more unrelated sections in the rail. */ +.ws-npc { + padding: 7px 9px; + margin-bottom: 7px; + border: 1px solid var(--border); + border-left: 2px solid var(--accent-dim); + border-radius: 7px; + background: rgba(255, 255, 255, 0.022); +} +.ws-npc:last-child { margin-bottom: 0; } +.ws-npc-head { display: flex; align-items: center; gap: 7px; margin-bottom: 3px; } +.ws-npc-avatar { + flex-shrink: 0; + width: 22px; height: 22px; + border-radius: 50%; + display: grid; place-items: center; + font-family: var(--font-display); + font-size: 0.6rem; + color: #14100a; + background: linear-gradient(150deg, var(--accent-bright), var(--accent-dim)); +} +.ws-npc-name { + font-family: var(--font-display); + font-size: 0.85rem; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.ws-none { font-size: 0.78rem; color: var(--text-dim); font-style: italic; } +.ws-milestones { list-style: none; margin: 0; padding: 0; } +.ws-milestones li { + display: flex; + gap: 7px; + align-items: flex-start; + padding: 4px 0; + font-size: 0.85rem; + color: var(--text); +} +.ws-milestones li.done { color: var(--text-dim); } +.ws-check { flex-shrink: 0; color: var(--accent-bright); } +.ws-milestones li.done .ws-check { color: var(--player); } +.ws-flags { list-style: none; margin: 0; padding: 0; } +.ws-flags li { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + font-size: 0.85rem; + color: var(--text); +} +.ws-flag-dot { + width: 9px; height: 9px; border-radius: 50%; + flex-shrink: 0; + background: var(--bg-input); + border: 1px solid var(--border); +} +.ws-flag-dot.on { background: var(--accent); border-color: var(--accent); } +.ws-flag-name { flex: 1; } +.ws-flag-val { color: var(--text-dim); font-size: 0.8rem; } +.ws-flag-edit { display: flex; align-items: center; gap: 8px; cursor: pointer; width: 100%; } + +.ws-edit-actions { display: flex; gap: 6px; } +.ws-edit-input { + width: 100%; + margin-top: 3px; + padding: 4px 7px; + font-size: 0.82rem; + border-radius: 6px; +} + +.schema-editor { + width: 100%; + font-family: var(--font-mono, monospace); + font-size: 0.82rem; + line-height: 1.5; + tab-size: 2; + white-space: pre; + overflow-x: auto; +} +.schema-error { + margin-top: 6px; + color: var(--danger, #e5484d); + font-size: 0.82rem; +} +.schema-json-actions { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; +} +.schema-json-actions .schema-error { margin-top: 0; } +.schema-preview { + margin-top: 14px; + padding: 14px 16px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-panel); +} +.schema-preview-title { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--text-dim); + margin-bottom: 10px; +} +.schema-group { margin-bottom: 16px; } +.schema-group:last-child { margin-bottom: 0; } +.schema-group-title { + font-family: Georgia, serif; + font-size: 0.98rem; + color: var(--accent-bright); + margin-bottom: 6px; +} +.schema-npc-id { + font-family: var(--font-mono, monospace); + font-size: 0.72rem; + color: var(--text-dim); + margin-left: 6px; +} +.schema-stat { padding: 4px 0; border-bottom: 1px solid var(--border); } +.schema-stat:last-child { border-bottom: none; } +.schema-stat-head { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; +} +.schema-stat-name { color: var(--text); text-transform: capitalize; } +.schema-stat-meta { + color: var(--text-dim); + font-size: 0.8rem; + font-variant-numeric: tabular-nums; + text-align: right; +} +.schema-desc { color: var(--text-dim); font-size: 0.8rem; margin-top: 2px; } +.schema-bands { color: var(--text-dim); font-size: 0.76rem; margin-top: 2px; font-style: italic; } + diff --git a/frontend/src/styles/forms.css b/frontend/src/styles/forms.css new file mode 100644 index 0000000..afe0f52 --- /dev/null +++ b/frontend/src/styles/forms.css @@ -0,0 +1,64 @@ +/* ---------- Forms ---------- */ + +label.field { display: block; margin-bottom: 14px; } +label.field .label { + display: block; + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--text-dim); + margin-bottom: 5px; +} +input[type="text"], input[type="email"], input[type="password"], input[type="number"], textarea, select { + width: 100%; + background: var(--bg-input); + color: var(--text); + border: 1px solid var(--border); + border-radius: 8px; + padding: 9px 12px; + font-size: 0.95rem; + font-family: inherit; + transition: border-color 0.15s, box-shadow 0.15s; +} +textarea { resize: vertical; min-height: 70px; line-height: 1.5; } +input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--accent-dim); + box-shadow: 0 0 0 3px rgba(212, 169, 78, 0.12); +} +input::placeholder, textarea::placeholder { color: #5c584d; } + +button { + background: var(--bg-input); + color: var(--text); + border: 1px solid var(--border); + border-radius: 8px; + padding: 7px 16px; + font-size: 0.88rem; + font-weight: 500; + cursor: pointer; + font-family: inherit; + transition: border-color 0.15s, background 0.15s, box-shadow 0.15s, transform 0.1s; +} +button:hover { + border-color: var(--accent-dim); + box-shadow: 0 0 12px rgba(212, 169, 78, 0.1); +} +button:active { transform: translateY(1px); } +button.primary { + background: linear-gradient(160deg, var(--accent-bright), var(--accent) 55%, #b78d3e); + border-color: var(--accent); + color: #191307; + font-weight: 600; + letter-spacing: 0.02em; + box-shadow: 0 2px 10px var(--accent-glow); +} +button.primary:hover { + box-shadow: 0 2px 18px rgba(212, 169, 78, 0.45); + filter: brightness(1.08); +} +button.danger { color: var(--danger); } +button.danger:hover { border-color: var(--danger); box-shadow: 0 0 12px rgba(208, 101, 101, 0.15); } +button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: none; } + diff --git a/frontend/src/styles/insights.css b/frontend/src/styles/insights.css new file mode 100644 index 0000000..2998733 --- /dev/null +++ b/frontend/src/styles/insights.css @@ -0,0 +1,203 @@ +/* ---------- Insights ---------- */ + +.insights-meta { margin-bottom: 16px; font-size: 0.85rem; } +.insights-mode { color: var(--text-dim); margin-bottom: 8px; } +.insights-mode button { padding: 2px 8px; font-size: 0.75rem; } +.token-total { font-size: 1rem; color: var(--text); margin-bottom: 4px; } +.token-total.over { color: var(--danger); } +/* Stacked share-of-context bar. Segment colours come from SECTION_COLORS in + Play.jsx so the bar, its legend and each section header stay in sync. */ +.token-split { + display: flex; + height: 14px; + background: var(--bg-input); + border-radius: 4px; + overflow: hidden; + margin-bottom: 8px; +} +.token-split.over { box-shadow: 0 0 0 1px var(--danger); } +.token-seg { + min-width: 2px; /* a 0.2% section is still worth seeing */ + border-right: 1px solid var(--bg-panel); + transition: opacity 0.15s ease; +} +.token-seg:last-child { border-right: none; } +.token-seg-free { background: transparent; min-width: 0; border-right: none; } +.token-seg.faded, .token-legend-item.faded { opacity: 0.25; } + +.token-legend { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); + gap: 1px 12px; + margin-bottom: 10px; +} +.token-legend-item { + display: flex; + align-items: center; + gap: 7px; + background: none; + border: none; + border-radius: 4px; + padding: 2px 5px; + font-size: 0.78rem; + color: var(--text-dim); + text-align: left; + transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease; +} +.token-legend-item:hover { background: rgba(255, 255, 255, 0.05); color: var(--text); } +.token-swatch { width: 9px; height: 9px; border-radius: 2px; flex: none; } +.token-legend-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.token-legend-pct { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; } +.token-legend-tok { + width: 4.5em; + text-align: right; + opacity: 0.65; + font-variant-numeric: tabular-nums; +} +.token-legend-more { + justify-self: start; + background: none; + border: none; + padding: 2px 5px; + font-size: 0.75rem; + color: var(--text-dim); +} +.token-legend-more:hover { color: var(--accent); background: none; } +.insights-history { color: var(--text-dim); margin-bottom: 8px; } +.insights-cards div { margin-bottom: 3px; } +.insights-cards .dropped { color: var(--danger); } + +.ctx-section { margin-bottom: 12px; border-left: 3px solid var(--border); background: rgba(255, 255, 255, 0.015); border-radius: 0 6px 6px 0; } +.ctx-section .ctx-header { + display: flex; + justify-content: space-between; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-dim); + padding: 5px 10px 3px; +} +.ctx-section pre { + margin: 0; + padding: 6px 10px 8px; + white-space: pre-wrap; + word-break: break-word; + font-family: Consolas, monospace; + font-size: 0.8rem; + line-height: 1.5; + color: var(--text); + max-height: 260px; + overflow-y: auto; +} +/* Per-section border/header colours are applied inline from SECTION_COLORS + (Play.jsx) — one palette drives the bar, the legend and these headers. */ + +/* ---------- Scripts ---------- */ + +.slot-tabs { display: flex; gap: 0; margin-bottom: 4px; } +.slot-tabs button { + border-radius: 0; + border-right-width: 0; + color: var(--text-dim); +} +.slot-tabs button:first-child { border-radius: 8px 0 0 8px; } +.slot-tabs button:last-child { border-radius: 0 8px 8px 0; border-right-width: 1px; } +.slot-tabs button.active { color: var(--accent-bright); background: var(--bg-panel); } +.slot-hint { color: var(--text-dim); font-size: 0.8rem; margin-bottom: 8px; } + +.cm-editor { border: 1px solid var(--border); border-radius: 8px; font-size: 0.88rem; } +.cm-editor.cm-focused { + outline: none; + border-color: var(--accent-dim); + box-shadow: 0 0 0 3px rgba(212, 169, 78, 0.1); +} + +.test-run .row { display: flex; gap: 12px; } +.test-result { + margin-top: 12px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 8px; + padding: 10px 12px; + font-size: 0.85rem; +} +.test-result .label { + text-transform: uppercase; + font-size: 0.7rem; + letter-spacing: 0.08em; + color: var(--text-dim); + margin-right: 6px; +} +.test-result pre { + display: inline-block; + margin: 2px 0; + white-space: pre-wrap; + word-break: break-word; + font-family: Consolas, monospace; + vertical-align: top; +} +.test-error { color: var(--danger); white-space: pre-wrap; } + +.script-attach { + display: flex; + align-items: baseline; + gap: 8px; + padding: 9px 12px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 8px; + margin-bottom: 6px; + cursor: pointer; + transition: border-color 0.15s; +} +.script-attach:hover { border-color: var(--border-bright); } +.script-attach .dim { color: var(--text-dim); font-size: 0.82rem; } + +/* ---------- Memory bank (Phase 6) ---------- */ + +.memory-row { + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 8px; + padding: 9px 12px; + margin-bottom: 6px; +} +.memory-row.forgotten { opacity: 0.55; } +.memory-text { font-size: 0.9rem; white-space: pre-wrap; } +.memory-meta { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 10px; + margin-top: 5px; + font-size: 0.78rem; +} +.memory-meta .dim { color: var(--text-dim); } +.memory-meta .action-tools { display: inline-flex; gap: 4px; margin-left: auto; } +.memory-meta .action-tools button { + padding: 1px 8px; + font-size: 0.72rem; + color: var(--text-dim); +} +.memory-meta .action-tools button:hover { color: var(--text); } +.memory-badge { + color: var(--accent); + border: 1px solid var(--border); + border-radius: 999px; + padding: 0 8px; +} + +.script-report { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 8px; } +.script-error { color: var(--danger); font-size: 0.85rem; margin-bottom: 4px; white-space: pre-wrap; } +.script-logs { + background: var(--bg-input); + border-radius: 8px; + padding: 8px 10px; + font-family: Consolas, monospace; + font-size: 0.8rem; + white-space: pre-wrap; + word-break: break-word; + margin: 6px 0; +} + diff --git a/frontend/src/styles/modals.css b/frontend/src/styles/modals.css new file mode 100644 index 0000000..41b9cb7 --- /dev/null +++ b/frontend/src/styles/modals.css @@ -0,0 +1,57 @@ +/* ---------- Filter bar & tags ---------- */ + +.filter-bar { margin-bottom: 18px; } +.search-input { max-width: 340px; } +.tag-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; } +.tag { + padding: 2px 12px; + font-size: 0.76rem; + border-radius: 999px; + color: var(--text-dim); +} +.tag.active { + color: var(--accent-bright); + border-color: var(--accent-dim); + box-shadow: inset 0 0 10px rgba(212, 169, 78, 0.1); +} +.tag.small { + display: inline-block; + background: var(--bg-input); + border: 1px solid var(--border); + padding: 1px 9px; + margin-right: 4px; + cursor: default; +} + +/* ---------- Modal ---------- */ + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(5, 5, 9, 0.7); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 50; + animation: page-in 0.2s ease both; +} +.modal { + width: min(480px, calc(100vw - 40px)); + max-height: 80vh; + overflow-y: auto; + background: var(--bg-panel); + border: 1px solid var(--border-bright); + border-radius: 14px; + padding: 26px 28px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6), 0 0 40px rgba(212, 169, 78, 0.06); +} +.modal h2 { + margin: 0 0 4px; + font-family: var(--font-display); + font-size: 1.2rem; + color: var(--accent-bright); +} +.modal .modal-hint { margin: 0 0 18px; color: var(--text-dim); font-size: 0.88rem; } +.modal .modal-buttons { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; } + diff --git a/frontend/src/styles/nav.css b/frontend/src/styles/nav.css new file mode 100644 index 0000000..67a271a --- /dev/null +++ b/frontend/src/styles/nav.css @@ -0,0 +1,120 @@ +/* ---------- Top navigation ---------- */ + +.topnav { + display: flex; + align-items: center; + gap: 26px; + padding: 0 28px; + height: 56px; + background: var(--bg-panel-glass); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border); + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); + position: sticky; + top: 0; + z-index: 10; +} +.topnav .brand { + font-family: var(--font-display); + font-size: 1.2rem; + font-weight: 700; + color: var(--accent); + letter-spacing: 0.14em; + text-shadow: 0 0 18px var(--accent-glow); + margin-right: 6px; +} +.topnav a.navlink { + color: var(--text-dim); + padding: 17px 2px 15px; + font-size: 0.9rem; + font-weight: 500; + letter-spacing: 0.04em; + border-bottom: 2px solid transparent; + transition: color 0.15s, border-color 0.2s; +} +.topnav a.navlink.active { + color: var(--accent-bright); + border-bottom-color: var(--accent); + text-shadow: 0 0 14px var(--accent-glow); +} +.topnav a.navlink:hover { color: var(--text); } + +/* On desktop the wrapper is transparent — its children (nav links + account) + participate directly in the .topnav flex row, exactly as before. The mobile + query below turns it into a dropdown. */ +.nav-links { display: contents; } +.nav-hamburger { + display: none; + background: none; + border: none; + color: var(--text); + font-size: 1.2rem; + padding: 6px 8px; + margin-left: auto; + line-height: 1; +} + +/* Phase 8: account area (hosted multi-user mode only) */ +.nav-account { + margin-left: auto; + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} +.nav-account button { padding: 5px 12px; font-size: 0.82rem; } +.guest-nudge { + color: var(--text-dim); + font-size: 0.78rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.account-email { + color: var(--text-dim); + font-size: 0.82rem; + max-width: 220px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +button.linklike { + background: none; + border: none; + padding: 0; + color: var(--accent); + font-size: 0.82rem; + cursor: pointer; +} +button.linklike:hover { text-decoration: underline; } +@media (max-width: 900px) { + .guest-nudge { display: none; } +} + +/* Phase 8: demo-key banner (Settings) */ +.demo-banner { + border: 1px solid var(--border-bright); + border-left: 3px solid var(--accent); + border-radius: 8px; + padding: 12px 16px; + margin-bottom: 18px; + color: var(--text-dim); + font-size: 0.88rem; + line-height: 1.5; +} +.demo-banner strong { color: var(--accent-bright); } + +.page { + flex: 1; + width: 100%; + max-width: 960px; + margin: 0 auto; + padding: 28px 24px; + animation: page-in 0.35s ease both; +} +@keyframes page-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: none; } +} + diff --git a/frontend/src/styles/panels.css b/frontend/src/styles/panels.css new file mode 100644 index 0000000..45c3a3f --- /dev/null +++ b/frontend/src/styles/panels.css @@ -0,0 +1,65 @@ +/* ---------- Side panel (Plot / Insights) ---------- */ + +.play-layout { flex: 1; display: flex; min-height: calc(100dvh - 56px); } +.play-layout .play-page { max-width: 960px; } +.play-layout.with-panel .play-page { max-width: none; } + +.panel-toggles { display: flex; gap: 8px; } +.panel-toggles button.active { + color: var(--accent-bright); + border-color: var(--accent-dim); + box-shadow: inset 0 0 12px rgba(212, 169, 78, 0.08); +} + +.side-panel { + width: 420px; + flex-shrink: 0; + border-left: 1px solid var(--border); + background: var(--bg-panel); + padding: 20px; + overflow-y: auto; + height: calc(100dvh - 56px); + position: sticky; + top: 56px; + animation: panel-in 0.25s ease both; +} + +/* ---------- In-play script viewer ---------- */ + +.adv-script { + border-bottom: 1px solid var(--border); + padding: 10px 0; +} +.adv-script-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; } +.adv-script-desc { font-size: 0.82rem; margin-top: 4px; } +.linklike { + border: none; + background: none; + padding: 0; + color: var(--accent-bright); + font-size: 0.8rem; + cursor: pointer; + flex-shrink: 0; +} +.linklike:hover { text-decoration: underline; } +.adv-script-code { margin-top: 8px; } +.adv-script-hook { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-dim); + margin: 8px 0 2px; +} +.adv-script-code pre { + margin: 0; + padding: 10px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 6px; + overflow-x: auto; + font-family: var(--font-mono, monospace); + font-size: 0.76rem; + line-height: 1.5; + white-space: pre; +} + diff --git a/frontend/src/styles/play.css b/frontend/src/styles/play.css new file mode 100644 index 0000000..117c668 --- /dev/null +++ b/frontend/src/styles/play.css @@ -0,0 +1,316 @@ +/* ---------- Play page ---------- */ + +.play-page { + display: flex; + flex-direction: column; + min-height: calc(100dvh - 56px); + /* Room to scroll the last beat clear of a composer lifted by the keyboard + (see --kb-inset in keyboard.js); 0 on desktop and on browsers that shrink + the layout viewport themselves. Margin, not padding — .page already sets + padding and a longhand here would drop its bottom gutter. */ + margin-bottom: var(--kb-inset, 0px); +} + +/* Keep the Plot/Memory/Scripts/Insights toggles reachable while the story + scrolls — pin the header just below the 56px top nav. */ +.play-page .page-header { + position: sticky; + top: 56px; + z-index: 5; + background: var(--bg); + margin-bottom: 12px; + padding: 12px 0; + border-bottom: 1px solid var(--border); +} + +.story { + flex: 1; + font-family: var(--font-story); + font-size: 1.14rem; + line-height: 1.7; + white-space: pre-wrap; + padding-bottom: 16px; +} +/* The head of a windowed transcript: how much story is still above, and the + way back to it. Deliberately quiet — it is a seam in the page, not a + feature — and it sits inside .story so it inherits the story face. */ +.story-earlier { + text-align: center; + margin: 4px 0 22px; + font-size: 0.9rem; + /* No action-in animation here: this appears above content the reader is + already looking at, and a fade would read as the story moving. */ +} +.story-earlier .dim { color: var(--text-dim); font-style: italic; } +.story-earlier button { + background: none; + border: none; + border-bottom: 1px solid rgba(159, 199, 209, 0.3); + color: var(--text-dim); + font: inherit; + font-style: italic; + cursor: pointer; + padding: 2px 4px; +} +.story-earlier button:hover { + color: var(--accent-bright); + border-bottom-color: var(--accent-bright); +} +.story .action { + position: relative; + margin-bottom: 15px; + padding-right: 56px; + animation: action-in 0.4s ease both; +} +@keyframes action-in { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: none; } +} +.story .action.player { + color: var(--player); + border-left: 2px solid rgba(159, 199, 209, 0.35); + padding-left: 14px; + font-style: italic; +} +.story .action.streaming { color: var(--text); } +.story .cursor { + color: var(--accent-bright); + text-shadow: 0 0 8px var(--accent-glow); + animation: blink 1s steps(1) infinite; +} +@keyframes blink { 50% { opacity: 0; } } + +/* Reasoning-model thinking, collapsed above the AI's response */ +.story .reasoning { + margin: 0 0 8px; + font-size: 0.82rem; + line-height: 1.5; + color: var(--text-dim); + font-style: normal; +} +.story .reasoning summary { + cursor: pointer; + user-select: none; + color: var(--text-dim); + opacity: 0.8; +} +.story .reasoning summary:hover { opacity: 1; } +.story .reasoning .reasoning-text { + margin-top: 6px; + padding: 8px 12px; + border-left: 2px solid var(--border); + opacity: 0.85; + max-height: 260px; + overflow-y: auto; +} + +.story .action-tools { + position: absolute; + top: 0; + right: 0; + display: inline-flex; + gap: 4px; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s; +} +.story .action:hover .action-tools { opacity: 1; pointer-events: auto; } +.story .action-tools button { + padding: 1px 8px; + font-size: 0.72rem; + font-family: var(--font-ui); + color: var(--text-dim); + background: var(--bg-panel-glass); + backdrop-filter: blur(6px); +} +.story .action-tools button:hover { color: var(--text); } + +/* The takes of one turn: ‹ 2/4 ›. Stays quiet until hovered — it's a footnote + on the message, not part of the prose. + + Small and unemphatic on purpose. Stepping through takes reads them and does + nothing else, so it should not carry the visual weight of a decision; the + decision is made by writing, which happens in the composer below. */ +.take-pager { + display: flex; + align-items: center; + gap: 2px; + margin-top: 6px; + font-family: var(--font-ui); + opacity: 0.45; + transition: opacity 0.15s; +} +.story .action:hover .take-pager, +.take-pager:focus-within { opacity: 1; } +.take-pager button { + width: 20px; + height: 20px; + padding: 0; + font-size: 0.9rem; + line-height: 1; + color: var(--text-dim); + background: transparent; + border: 1px solid transparent; + border-radius: 4px; +} +.take-pager button:hover:not(:disabled) { + color: var(--text); + border-color: var(--border-bright); +} +.take-pager button:disabled { opacity: 0.3; cursor: default; } +.take-count { + min-width: 30px; + text-align: center; + font-size: 0.72rem; + font-variant-numeric: tabular-nums; + color: var(--text-dim); +} +/* Only while a take that isn't the live one is on screen: it says what the + next thing typed will do, because that is the click that forks. */ +.take-note { + margin-left: 6px; + font-size: 0.72rem; + font-style: italic; + color: var(--accent-dim); +} + +/* ---------- Branches panel (Phase 14, SP7) ---------- */ + +.panel-empty { + padding: 18px 0; + color: var(--text-dim); + font-size: 0.85rem; +} +.branch-panel { display: flex; flex-direction: column; gap: 14px; } +.branch-intro { + margin: 0; + color: var(--text-dim); + font-size: 0.82rem; + line-height: 1.55; +} +.branch-list { display: flex; flex-direction: column; gap: 8px; } +.branch-row { + display: flex; + flex-direction: column; + gap: 5px; + padding: 9px 11px; + border: 1px solid var(--border); + border-radius: 5px; + background: var(--bg-input); +} +.branch-row.here { + border-color: var(--accent-dim); + background: rgba(212, 169, 78, 0.07); +} +.branch-head { display: flex; align-items: center; gap: 7px; } +.branch-glyph { color: var(--border-bright); font-size: 0.85rem; } +.branch-row.here .branch-glyph { color: var(--accent); } +.branch-name { + font-family: var(--font-story); + font-size: 0.98rem; + color: var(--text); +} +.branch-row.here .branch-name { color: var(--accent-bright); } +.branch-here { + margin-left: auto; + font-size: 0.64rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--accent-dim); +} +.branch-rename { + flex: 1; + min-width: 0; + padding: 3px 7px; + font-family: var(--font-story); + font-size: 0.95rem; +} +.branch-meta { + font-size: 0.72rem; + color: var(--text-dim); + font-variant-numeric: tabular-nums; +} +.branch-tools, .branch-confirm { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; } +.branch-tools button, .branch-confirm button { + padding: 3px 9px; + font-size: 0.72rem; + color: var(--text-dim); + background: transparent; + border: 1px solid var(--border); +} +.branch-tools button:hover:not(:disabled), +.branch-confirm button:hover:not(:disabled) { + color: var(--accent-bright); + border-color: var(--border-bright); +} +.branch-tools button.danger:hover:not(:disabled), +.branch-confirm button.danger:hover:not(:disabled) { + color: var(--danger); + border-color: var(--danger); +} +.branch-tools button:disabled, .branch-confirm button:disabled { opacity: 0.4; cursor: default; } +.branch-confirm span { + font-size: 0.74rem; + color: var(--danger); + flex-basis: 100%; +} + +.action-edit { margin-bottom: 14px; } +/* Sized by AutoTextarea to fit the text being edited — an AI beat is usually + several paragraphs, and the old fixed 110px turned that into a keyhole. + min-height keeps a short player action from collapsing; max-height stops a + very long beat from pushing Save/Cancel off the screen (it scrolls instead). + resize stays on, so the height is a starting point, not a cage. */ +.action-edit textarea { + min-height: 110px; + max-height: 65vh; + overflow-y: auto; + font-family: inherit; + font-size: inherit; +} +.action-edit .edit-buttons { display: flex; gap: 8px; margin-top: 6px; } + +.play-controls { + position: sticky; + /* Sits on the keyboard's top edge when one is open, on the viewport bottom + otherwise — --kb-inset is 0 unless a virtual keyboard is covering us. */ + bottom: var(--kb-inset, 0px); + background: linear-gradient(to top, var(--bg) 75%, transparent); + padding: 18px 0 16px; + border-top: 1px solid var(--border); +} +.turn-buttons { display: flex; gap: 8px; margin-bottom: 10px; } +.turn-buttons button { font-size: 0.78rem; padding: 4px 12px; color: var(--text-dim); } +.turn-buttons button:hover:not(:disabled) { color: var(--text); } + +.input-bar { display: flex; gap: 8px; align-items: flex-end; } +.input-bar input { flex: 1; } +/* Auto-growing action box: starts at one line, grows to ~4, then scrolls. */ +.input-bar textarea { + flex: 1; + resize: none; + min-height: 0; + max-height: calc(6em + 20px); /* ~4 lines (1.5em each) + padding/border */ + overflow-y: auto; + line-height: 1.5; +} + +.mode-select { display: flex; } +.mode-select button { + border-radius: 0; + border-right-width: 0; + padding: 7px 14px; + color: var(--text-dim); + text-transform: capitalize; +} +.mode-select button:first-child { border-radius: 8px 0 0 8px; } +.mode-select button:last-child { border-radius: 0 8px 8px 0; border-right-width: 1px; } +.mode-select button.active { + color: var(--accent-bright); + background: var(--bg-panel); + border-color: var(--accent-dim); + border-right-width: 1px; + box-shadow: inset 0 0 12px rgba(212, 169, 78, 0.08); +} +.mode-select button.active + button { border-left-width: 0; } + diff --git a/frontend/src/styles/responsive.css b/frontend/src/styles/responsive.css new file mode 100644 index 0000000..61e2a68 --- /dev/null +++ b/frontend/src/styles/responsive.css @@ -0,0 +1,219 @@ +/* ============================================================ + Mobile / narrow screens (≤ 720px) + The desktop Play screen is a horizontal row of up to four columns + (two left rails + story + right side-panel). Phones can't fit that, + so here we collapse to a single story column and turn the rails and + side-panel into full-height overlays. Everything above this query is + the untouched desktop design. + ============================================================ */ +@media (max-width: 720px) { + + /* ---------- Top nav → hamburger dropdown ---------- */ + .topnav { gap: 12px; padding: 0 16px; } + .nav-hamburger { display: block; } + .nav-links { + display: none; + position: absolute; + top: 56px; + left: 0; + right: 0; + flex-direction: column; + align-items: stretch; + background: var(--bg-panel); + border-bottom: 1px solid var(--border); + box-shadow: 0 14px 34px rgba(0, 0, 0, 0.55); + padding: 4px 0; + } + .nav-links.open { display: flex; } + .topnav a.navlink { + padding: 13px 20px; + border-bottom: none; + border-left: 3px solid transparent; + } + .topnav a.navlink.active { + border-bottom-color: transparent; + border-left-color: var(--accent); + } + .nav-account { + margin-left: 0; + padding: 12px 20px; + border-top: 1px solid var(--border); + flex-wrap: wrap; + } + .account-email { max-width: 100%; } + + /* ---------- Global page padding / typography ---------- */ + .page { padding: 18px 14px; } + /* 16px inputs stop iOS Safari auto-zooming when a field is focused. */ + input[type="text"], input[type="email"], input[type="password"], input[type="number"], textarea, select { + font-size: 16px; + } + + /* ---------- Play screen → single column ---------- */ + .play-layout { flex-direction: column; } + .play-layout .play-page, + .play-layout.with-panel .play-page { max-width: 100%; } + .play-page .page-header h1 { font-size: 1.25rem; } + .panel-toggles { flex-wrap: wrap; } + .story { font-size: 1.06rem; } + /* Reserve a left gutter so the fixed World/State edge-tabs never sit on + top of the story text as it scrolls past them. */ + .story, .play-page .empty { padding-left: 26px; } + .story .action { padding-right: 0; } + /* No hover on touch — keep per-action edit/delete tools visible. */ + .story .action-tools { opacity: 1; pointer-events: auto; } + + /* ---------- Left rails (World State / Script State) → edge tabs + overlay ---------- */ + /* Closed: a compact tab pinned to the left edge instead of a full-height + column that steals the story's width. World State sits above Script State. + (World State only renders for RPG adventures, so it may be absent.) */ + .status-drawer { + position: fixed; + left: 0; + z-index: 40; + } + .status-drawer:not(.open) { + top: 170px; + height: auto; + background: var(--bg-panel); + border: 1px solid var(--border); + border-left: none; + border-radius: 0 10px 10px 0; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); + } + .ws-drawer:not(.open) { top: 170px; } + .status-drawer:not(.ws-drawer):not(.open) { top: 260px; } + .status-drawer:not(.open) .status-toggle { height: auto; padding: 14px 4px; } + + /* Open: slide-in overlay covering most of the screen. */ + .status-drawer.open { + top: 56px; + height: calc(100dvh - 56px); + width: min(88vw, 320px); + box-shadow: 0 0 50px rgba(0, 0, 0, 0.65); + border-right: 1px solid var(--border-bright); + } + + /* ---------- Right side-panel (Plot/Memory/Scripts/Insights) → full overlay ---------- */ + .side-panel { + position: fixed; + inset: 56px 0 0 0; + width: auto; + height: auto; + z-index: 45; + border-left: none; + padding: 16px; + } + + /* ---------- Home / library surfaces ---------- */ + + /* Both grids go single-column: the wide variant's 320px minimum would + otherwise overflow a 360px phone once card padding is counted. */ + .card-grid, .card-grid.wide { grid-template-columns: 1fr; } + + .hall { padding: 2px 0 20px; } + .hall-sub { font-size: 0.95rem; } + .home-section { margin-top: 26px; } + + /* The label eats most of a narrow line, so drop the short leading rule and + let the trailing one take whatever space is left. */ + .rule { letter-spacing: 0.16em; font-size: 0.76rem; gap: 10px; } + .rule::before { display: none; } + .rule-head { gap: 10px; } + + /* Ornament scales down rather than crowding the corners on a small card. */ + .card.tome { padding: 14px 15px 12px; } + .card.tome::before, .card.tome::after { width: 9px; height: 9px; } + /* Always-on filigree: there's no hover on touch. */ + .card.tome::before, .card.tome::after { opacity: 0.8; } + .seal { width: 21px; height: 21px; top: 10px; right: 10px; font-size: 0.54rem; } + .card.tome:has(> .seal) .card-head { padding-right: 22px; } + .snippet { font-size: 0.94rem; -webkit-line-clamp: 4; } + .snippet.dropcap::first-letter { font-size: 2rem; padding-right: 6px; } + + /* Motes are pure decoration — thin them out where the GPU is weakest and + the effect is least visible. (Embers.jsx already scales count to the + viewport; this drops the opacity too.) */ + .embers { opacity: 0.55; } + + /* Full-width toasts, clear of the sticky play controls. */ + .toast-host { + left: 12px; + right: 12px; + bottom: 14px; + transform: none; + width: auto; + max-width: none; + align-items: stretch; + } + .toast { font-size: 0.82rem; } + + /* Art picker stacks; the emoji grid stays tappable at 34px minimum. */ + .art-picker { gap: 12px; } + .art-preview { width: 64px; height: 64px; } + .art-controls { min-width: 0; } + + /* The drop cap on the opening beat competes with the edge tabs at this + width, so shrink it and keep it inside the 26px story inset. */ + .story .action.opening::first-letter { + font-size: 2.5rem; + padding: 4px 8px 0 0; + } + + /* ---------- Composers: give the typing area the full width ---------- + Both composers are one flex row on desktop, which on a phone left the + textarea with a sliver: the Play bar spends ~180px on Do/Say/Story plus + ~75px on Send, and the chat bar ~90px on Send. Letting the row wrap and + moving the buttons onto their own line hands that width back to the + textarea, which is the part you actually type into. + + Play: Do/Say/Story and Send share the top row (Send pushed to the right + edge), and `order` drops the textarea onto its own full-width row beneath + them. Two rows, and the typing area goes from ~138px to the full width — + measured at a 390px composer. */ + .input-bar { flex-wrap: wrap; } + .mode-select button { flex: 1; padding: 7px 10px; } + .input-bar > button { margin-left: auto; } + /* min-width:0 lets a flex item shrink below its content width. */ + .input-bar textarea { order: 1; flex: 1 0 100%; margin-top: 8px; min-width: 0; } + + /* ---------- AI Chat ---------- */ + /* No hover on touch, so the per-message copy/delete links stay visible. */ + .chat-msg-actions { opacity: 1; } + .chat-msg { padding: 9px 11px 11px; } + .chat-composer { padding: 10px 0 14px; flex-wrap: wrap; } + /* Full-width textarea; Send wraps beneath it and sits at the right edge. */ + .chat-composer textarea { flex: 1 1 100%; max-height: 140px; } + .chat-composer button { margin-left: auto; } + + /* ---------- The branch map ---------- */ + /* A phone has no room for a dialog inset from the edges, and the tree is the + one thing on this screen that wants every pixel across. */ + .branch-map { + width: 100vw; + max-width: none; + max-height: 100dvh; + height: 100dvh; + border: none; + border-radius: 0; + } + .branch-map-header { padding: 14px 16px 10px; flex-wrap: wrap; } + .branch-map-canvas { padding: 4px 6px 8px; } + .branch-map-detail { padding: 12px 16px 16px; } + .branch-map-hint { padding: 0 16px 10px; } + + /* ---------- Analytics dashboard ---------- */ + .an-page .page-header { flex-direction: column; align-items: flex-start; gap: 10px; } + .an-tiles { gap: 8px; } + .an-tile { padding: 10px 12px; } + .an-tile-value { font-size: 1.35rem; } + .an-grid { grid-template-columns: 1fr; } + .an-plot { height: 140px; } + .an-tabs button { padding: 8px 10px; font-size: 0.78rem; } + .an-log-search { max-width: none; width: 100%; } + /* Three columns don't fit; the label goes above its own bar instead. */ + .an-funnel-row { grid-template-columns: 1fr auto; gap: 4px 10px; } + .an-funnel-track { grid-column: 1; } + .an-funnel-label { grid-column: 1 / -1; } + .an-funnel-value { grid-column: 2; } +} diff --git a/frontend/src/styles/schema-editor.css b/frontend/src/styles/schema-editor.css new file mode 100644 index 0000000..dacc093 --- /dev/null +++ b/frontend/src/styles/schema-editor.css @@ -0,0 +1,243 @@ +/* ---- Form-based schema editor ---- + One shape for everything: a section is a panel, every entry inside it is a + `.se-row` tile, and every control wears a `.se-field` caption. Adding a new + kind of entry means reusing those three, not inventing a fourth box. */ +.se-section { + margin-bottom: 16px; + padding: 13px 15px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--bg-panel); +} +.se-section-nested { + margin: 10px 0 0; + padding: 11px 12px; + background: rgba(0, 0, 0, 0.22); + border-radius: 8px; +} +.se-section-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; +} +.se-section-title { + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--accent); +} +.se-hint { + margin: -2px 0 10px; + font-size: 0.78rem; + line-height: 1.45; + color: var(--text-dim); +} +.se-empty { color: var(--text-dim); font-size: 0.82rem; padding: 4px 0; } +.se-add, .se-add-sm { + font-size: 0.78rem; + padding: 3px 10px; + white-space: nowrap; +} +.se-add-sm { margin-top: 6px; } +.se-remove { + background: transparent; + border: none; + color: var(--text-dim); + cursor: pointer; + padding: 2px 6px; + font-size: 0.9rem; + line-height: 1; + flex-shrink: 0; +} +.se-remove:hover { color: var(--danger, #e5484d); } + +/* Every control in the editor, one look. The global field rule keys off + `input[type="…"]`, so the plain ``s here (keys, descriptions) never + matched it and fell back to browser defaults — that mismatch is most of why + this panel used to read as a pile of unrelated boxes. Everything below is + qualified with `.se` so it outranks that global rule (0,1,1) too. */ +/* `:where()` so the exclusion costs no specificity — a plain + `:not([type="checkbox"])` would rank this above the `.se .se-*` overrides + below and swallow every one of them. */ +.se input:not(:where([type="checkbox"])), .se textarea, .se select { + width: 100%; + background: var(--bg-input); + color: var(--text); + border: 1px solid var(--border); + border-radius: 6px; + padding: 5px 8px; + font-size: 0.84rem; + font-family: inherit; + transition: border-color 0.15s, box-shadow 0.15s; +} + +/* One entry — a stat, a flag, a milestone. */ +.se-row { + padding: 10px 12px; + margin-bottom: 8px; + border: 1px solid var(--border); + border-radius: 8px; + background: rgba(255, 255, 255, 0.018); + transition: border-color 0.15s; +} +.se-row:last-child { margin-bottom: 0; } +.se-row:focus-within { border-color: var(--border-bright); } +/* Wraps because a phone-width row can't fit key + toggle + ✕ side by side, and + an input's default `min-width: auto` would push the ✕ off-screen instead. */ +.se-row-top { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 8px; } +.se .se-key { + font-family: var(--font-mono, monospace); + font-size: 0.82rem; + min-width: 0; + max-width: 220px; +} +/* Only in the row context — `.se-field` is a *column* flex, where a basis of + 140px would set the input's height instead of its width. */ +.se .se-row-top .se-key { flex: 1 1 140px; } +.se-field { display: flex; flex-direction: column; gap: 3px; } +.se-field > span { + font-size: 0.66rem; + text-transform: uppercase; + letter-spacing: 0.09em; + color: var(--text-dim); +} +.se-fields { + display: flex; + flex-wrap: wrap; + gap: 8px 12px; + align-items: flex-end; + margin-bottom: 8px; +} +.se .se-num input, .se .se-num select { width: 72px; padding: 4px 6px; } +.se .se-kind select { width: 132px; } +.se .se-initial-text input { width: 220px; max-width: 100%; } +.se-check { + display: flex; align-items: center; gap: 5px; + font-size: 0.8rem; color: var(--text-dim); + white-space: nowrap; +} +.se-check input { width: auto; } +/* The global textarea rule also forces min-height:70px, which a 2-row inline + field doesn't want. */ +.se .se-area { min-height: 0; line-height: 1.45; resize: vertical; } +.se-sub-head { font-size: 0.7rem; color: var(--text-dim); margin: 4px 0 4px; } +.se-band { display: flex; gap: 6px; align-items: center; margin-bottom: 4px; } +/* Qualified so it outweighs the global input[type="number"] rule (0,1,1) — + as a bare class it lost, and the band bounds rendered full-width. */ +.se .se-band-n { width: 64px; padding: 4px 6px; } +.se .se-band-l { flex: 1 1 60px; width: auto; min-width: 0; } +.se-bands { margin-top: 8px; } + +/* ---- NPC roster (its own page section) ---- */ +.se-npc-grid { + display: grid; + /* min() so a 320px phone gets one column instead of a 340px overflow */ + grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr)); + gap: 14px; + align-items: start; +} +.se-npc { + border: 1px solid var(--border); + border-radius: 10px; + background: var(--bg-panel); + overflow: hidden; +} +.se-npc:focus-within { border-color: var(--border-bright); } +.se-npc-head { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-bottom: 1px solid var(--border); + background: linear-gradient(180deg, rgba(212, 169, 78, 0.07), transparent); +} +.se-avatar { + flex-shrink: 0; + width: 34px; height: 34px; + border-radius: 50%; + display: grid; place-items: center; + font-family: var(--font-display); + font-size: 0.8rem; + letter-spacing: 0.02em; + color: #14100a; + background: linear-gradient(150deg, var(--accent-bright), var(--accent-dim)); +} +.se-npc-ident { flex: 1; min-width: 0; } +.se .se-npc-name { + padding: 2px 6px; + font-family: var(--font-display); + font-size: 0.95rem; + background: transparent; + border-color: transparent; +} +.se .se-npc-name:focus { background: var(--bg-input); border-color: var(--border-bright); } +.se-npc-addr { + display: block; + margin: 1px 0 0 6px; + font-family: var(--font-mono, monospace); + font-size: 0.7rem; + color: var(--text-dim); +} +.se-npc-count { + flex-shrink: 0; + font-size: 0.7rem; + color: var(--text-dim); + white-space: nowrap; +} +.se-npc-body { padding: 12px; display: flex; flex-direction: column; gap: 10px; } +.se-npc-idfield .se-key { max-width: 100%; } +.ws-report { list-style: none; margin: 4px 0 0; padding: 0; font-size: 0.82rem; } +.ws-report li { padding: 2px 0; color: var(--text); } + +/* Inline per-turn change chips under an AI message */ +.turn-changes { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 8px; +} +.chg { + font-size: 0.74rem; + padding: 2px 8px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--bg-input); + color: var(--text-dim); + font-variant-numeric: tabular-nums; + /* A chip must never be wider than the column it sits in: an NPC stat like + "bandit leader aggression +10" overruns a phone, and the chip row is + inside the story text, which has nothing to scroll. The label may break + mid-word as a last resort; the value is glued to it by its own nowrap so + the sign and number never end up on separate lines. */ + max-width: 100%; + overflow-wrap: anywhere; +} +.chg-val { white-space: nowrap; } +.chg-up { color: var(--player, #4caf82); border-color: color-mix(in srgb, var(--player, #4caf82) 40%, var(--border)); } +.chg-down { color: var(--danger, #e5484d); border-color: color-mix(in srgb, var(--danger, #e5484d) 40%, var(--border)); } +.chg-flag { color: var(--accent-bright); } +.chg-ms { color: var(--accent-bright); border-color: color-mix(in srgb, var(--accent-bright) 40%, var(--border)); } +/* A refused change, and a clamp that left the value where it started. The + dashed border separates it from the changes that took effect. It is not + styled as an error, because the rules refusing a change is them working. */ +.chg-refused { + border-style: dashed; + color: var(--text-dim); + border-color: color-mix(in srgb, var(--danger) 30%, var(--border)); +} +/* A clamp that reduced a change but still moved the value. */ +.chg-limited { opacity: 0.75; font-style: italic; } +.ws-report code { color: var(--accent-bright); } +.ws-delta { color: var(--player); font-variant-numeric: tabular-nums; } +.ws-flag { color: var(--text-dim); font-style: italic; } +.ws-rejected { color: var(--text-dim); } +.side-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } +.side-panel-header h2 { + margin: 0; + font-family: var(--font-display); + font-size: 1.05rem; + color: var(--accent); +} + diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css new file mode 100644 index 0000000..17e380a --- /dev/null +++ b/frontend/src/styles/tokens.css @@ -0,0 +1,50 @@ +:root { + --bg: #0a0a0f; + --bg-panel: #131320; + --bg-panel-glass: rgba(19, 19, 32, 0.82); + --bg-input: #1a1a2a; + --border: #2b2b3d; + --border-bright: #3d3d55; + --text: #e2ddd0; + --text-dim: #918c7d; + --accent: #d4a94e; + --accent-bright: #e8c476; + --accent-dim: #96773a; + --accent-glow: rgba(212, 169, 78, 0.25); + --danger: #d06565; + --player: #9fc7d1; + /* Chart hues (analytics dashboard). Deeper and more saturated than --accent + and --player, which are tuned for text and borders and turn muddy once + they are a 10px bar: these are checked against --bg-panel for lightness, + chroma, contrast and colour-blind separation as a set. */ + --chart-1: #b58a30; + --chart-2: #3d8ac4; + --chart-3: #a8608f; + --font-display: 'Cinzel', Georgia, serif; + --font-story: 'Crimson Pro', Georgia, 'Times New Roman', serif; + --font-ui: 'Inter', 'Segoe UI', system-ui, sans-serif; + font-family: var(--font-ui); + color-scheme: dark; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + background: + radial-gradient(1200px 700px at 15% -10%, rgba(212, 169, 78, 0.06), transparent 60%), + radial-gradient(1000px 600px at 90% 110%, rgba(88, 76, 140, 0.08), transparent 55%), + var(--bg); + background-attachment: fixed; + color: var(--text); + min-height: 100vh; +} + +#root { min-height: 100vh; display: flex; flex-direction: column; } + +h1, h2, h3 { font-weight: 600; letter-spacing: 0.03em; } +a { color: var(--accent); text-decoration: none; transition: color 0.15s; } +a:hover { color: var(--accent-bright); } + +::selection { background: var(--accent-dim); color: #0f0d08; } + diff --git a/frontend/src/styles/tome.css b/frontend/src/styles/tome.css new file mode 100644 index 0000000..c592564 --- /dev/null +++ b/frontend/src/styles/tome.css @@ -0,0 +1,475 @@ +/* ============================================================ + Illuminated tome — ambient layer, ornament, art plates, cards + Everything below styles the home/library surfaces. It comes after + the base .card rules on purpose: several selectors here tie with + earlier ones on specificity and rely on source order to win. + ============================================================ */ + +/* ---------- Ambient drifting motes ---------- */ + +/* Behind every page: fixed, unclickable, and out of the stacking flow. The + canvas is sized in JS (see Embers.jsx); z-index 0 with the app's own + surfaces at 1+ keeps it under content without needing a negative index + (which would drop it behind the body background). */ +.embers { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + opacity: 0.85; +} +/* .topnav already carries z-index 10 — deliberately not touched here, so it + keeps sitting above page content as well as above the canvas. */ +.page { position: relative; z-index: 1; } + +/* A third gradient pool warming the middle of the ground. `background` is a + shorthand, so background-attachment has to be restated or it resets to + scroll and the gradients start sliding with the page. */ +body { + background: + radial-gradient(1200px 700px at 15% -10%, rgba(212, 169, 78, 0.07), transparent 60%), + radial-gradient(1000px 600px at 90% 110%, rgba(88, 76, 140, 0.1), transparent 55%), + radial-gradient(760px 520px at 50% 42%, rgba(150, 119, 58, 0.05), transparent 62%), + var(--bg); + background-attachment: fixed; +} + +/* ---------- Home banner ---------- */ + +.hall { + display: flex; + flex-direction: column; + gap: 5px; + padding: 6px 0 26px; + margin-bottom: 4px; + border-bottom: 1px solid var(--border); + position: relative; +} +/* Gold hairline that fades out toward both ends, sat on the border above. */ +.hall::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--accent-dim) 22%, var(--accent-dim) 78%, transparent); + opacity: 0.55; +} +.hall-eyebrow { + margin: 0; + font-size: 0.68rem; + letter-spacing: 0.24em; + text-transform: uppercase; + color: var(--accent-dim); + font-weight: 600; +} +.hall-title { + margin: 0; + font-family: var(--font-display); + font-size: clamp(1.7rem, 4.6vw, 2.3rem); + font-weight: 600; + letter-spacing: 0.05em; + color: var(--accent-bright); + text-shadow: 0 0 30px var(--accent-glow); + text-wrap: balance; +} +.hall-sub { + margin: 0; + color: var(--text-dim); + font-family: var(--font-story); + font-size: 1.02rem; +} + +/* ---------- Ornamental section rules ---------- */ + +.home-section { margin-top: 34px; } + +.rule-head { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 16px; +} +.rule { + flex: 1; + display: flex; + align-items: center; + gap: 14px; + margin: 0; + font-family: var(--font-display); + font-size: 0.82rem; + font-weight: 600; + letter-spacing: 0.26em; + text-transform: uppercase; + color: var(--accent-bright); + white-space: nowrap; +} +/* Rules grow to fill whatever the label leaves — the label stays optically + centred in its own space without needing a fixed width. */ +.rule::before, .rule::after { + content: ''; + height: 1px; + flex: 1; + background: linear-gradient(90deg, transparent, var(--accent-dim) 45%, var(--accent-dim) 55%, transparent); +} +.rule::before { flex: 0 0 26px; } +.rule > span { flex: none; } + +button.linklike.ornate { + font-family: var(--font-display); + font-size: 0.74rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--accent-dim); + white-space: nowrap; + transition: color 0.15s; +} +button.linklike.ornate:hover { color: var(--accent-bright); text-decoration: none; } + +/* ---------- Art plates ---------- */ + +.art { + width: 38px; + height: 38px; + flex: none; + border-radius: 3px; + border: 1px solid var(--accent-dim); + box-shadow: inset 0 0 0 1px rgba(10, 10, 15, 0.7); + overflow: hidden; + display: grid; + place-items: center; + line-height: 1; +} +.art-lg { width: 46px; height: 46px; } +.art-photo img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.art-icon { font-size: 1.15rem; } +.art-lg.art-icon { font-size: 1.4rem; } +.art-mono { + font-family: var(--font-display); + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.06em; + color: rgba(232, 196, 118, 0.92); + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.65); +} +.art-lg.art-mono { font-size: 0.95rem; } + +/* ---------- Tome cards ---------- */ + +.card-grid.wide { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); } + +.card.tome { + border-radius: 3px; + padding: 16px 17px 14px; + background: + linear-gradient(168deg, rgba(212, 169, 78, 0.05), transparent 55%), + var(--bg-panel); + /* Inner dark gutter + a gold keyline: the double-rule border of a plate in + an old book, done with shadows so it costs no extra element. */ + box-shadow: + inset 0 0 0 1px rgba(10, 10, 15, 0.9), + inset 0 0 0 2px rgba(150, 119, 58, 0.22); +} + +/* Filigree corner brackets. These override the base .card::before gradient + bar (equal specificity, later in the file wins). */ +.card.tome::before, +.card.tome::after { + content: ''; + position: absolute; + width: 12px; + height: 12px; + border: 1px solid var(--accent-dim); + opacity: 0.5; + transition: opacity 0.2s; + background: none; + inset: auto; +} +.card.tome::before { + top: 6px; + left: 6px; + border-right: 0; + border-bottom: 0; +} +.card.tome::after { + bottom: 6px; + right: 6px; + border-left: 0; + border-top: 0; +} +.card.tome:hover::before, +.card.tome:hover::after { opacity: 1; } +.card.tome:hover { + border-color: var(--accent-dim); + box-shadow: + inset 0 0 0 1px rgba(10, 10, 15, 0.9), + inset 0 0 0 2px rgba(212, 169, 78, 0.4), + 0 12px 30px rgba(0, 0, 0, 0.5), + 0 0 26px rgba(212, 169, 78, 0.08); +} + +.card-head { display: flex; gap: 12px; align-items: center; margin-bottom: 10px; } +/* Keep headings clear of the wax seal, which is absolutely positioned over + the card's top-right corner. Only cards that actually carry one pay the + indent, so scenario cards keep their full width. */ +.card.tome:has(> .seal) .card-head { padding-right: 26px; } +.card-headings { min-width: 0; } +.card.tome h3 { + margin: 0; + font-family: var(--font-display); + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.045em; + color: var(--accent); + overflow-wrap: anywhere; +} +.card-from { + margin: 3px 0 0; + font-size: 0.75rem; + color: var(--text-dim); + overflow-wrap: anywhere; +} + +/* Wax seal marking a story in progress. */ +.seal { + position: absolute; + top: 12px; + right: 12px; + width: 24px; + height: 24px; + border-radius: 50%; + background: radial-gradient(circle at 34% 30%, #b8553f, #7d2f24 72%); + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.6), inset 0 -1px 3px rgba(0, 0, 0, 0.45); + display: grid; + place-items: center; + font-size: 0.6rem; + color: #f0d5b0; + z-index: 1; +} + +.snippet { + margin: 0; + font-family: var(--font-story); + font-size: 0.98rem; + line-height: 1.5; + color: #b3ac9c; + /* Clamp so a long beat can't stretch one card taller than its row. */ + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} +.snippet.muted { color: var(--text-dim); font-style: italic; } +.snippet.dropcap::first-letter { + float: left; + font-family: var(--font-display); + font-size: 2.3rem; + line-height: 0.85; + font-weight: 700; + color: var(--accent); + padding: 3px 7px 0 0; +} + +.card-foot { + display: flex; + align-items: center; + gap: 8px; + margin-top: 13px; + padding-top: 11px; + border-top: 1px solid var(--border); + font-size: 0.74rem; + color: var(--text-dim); + flex-wrap: wrap; +} +.card-foot .turns { font-variant-numeric: tabular-nums; } +.card-foot .dot { opacity: 0.5; } +.card-foot .resume { + margin-left: auto; + color: var(--accent-dim); + font-family: var(--font-ui); + letter-spacing: 0.1em; + text-transform: uppercase; + font-size: 0.68rem; + font-weight: 600; + transition: color 0.15s; +} +.card.tome:hover .resume { color: var(--accent-bright); } +.tag-cluster { display: flex; gap: 5px; flex-wrap: wrap; min-width: 0; } +.card-actions { margin-left: auto; display: inline-flex; gap: 5px; } + +button.tiny { padding: 2px 9px; font-size: 0.72rem; } +button.primary.compact { padding: 3px 12px; font-size: 0.76rem; margin-left: auto; } + +/* Staggered card entrance. The per-card delay is set inline. */ +.enter { animation: card-in 0.4s ease both; } +@keyframes card-in { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: none; } +} + +/* ---------- Loading skeletons ---------- */ + +.skeleton-card { + cursor: default; + pointer-events: none; + animation: card-in 0.3s ease both; +} +.skeleton-card::before, .skeleton-card::after { content: none; } +.skeleton-card:hover { transform: none; border-color: var(--border); box-shadow: none; } +.sk { + border-radius: 3px; + background: linear-gradient(90deg, #1c1c2b 25%, #262639 50%, #1c1c2b 75%); + background-size: 220% 100%; + animation: shimmer 1.4s ease-in-out infinite; +} +.sk-plate { width: 38px; height: 38px; margin-bottom: 12px; } +.sk-title { height: 13px; width: 58%; margin-bottom: 12px; } +.sk-line { height: 9px; margin-bottom: 8px; } +@keyframes shimmer { + from { background-position: 120% 0; } + to { background-position: -120% 0; } +} + +/* ---------- Global toasts ---------- */ + +.toast-host { + position: fixed; + bottom: 22px; + left: 50%; + transform: translateX(-50%); + z-index: 120; + display: flex; + flex-direction: column-reverse; + gap: 8px; + align-items: center; + width: max-content; + max-width: min(92vw, 520px); + pointer-events: none; +} +.toast { + pointer-events: auto; + display: flex; + align-items: flex-start; + gap: 9px; + text-align: left; + font-family: var(--font-ui); + font-size: 0.86rem; + line-height: 1.45; + padding: 10px 16px; + border-radius: 8px; + background: var(--bg-panel-glass); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--border-bright); + border-left: 3px solid var(--accent); + color: var(--text); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5); + animation: rise 0.25s ease both; +} +.toast:hover { border-color: var(--accent-dim); } +.toast-error { border-left-color: var(--danger); } +.toast-mark { color: var(--accent); flex: none; line-height: 1.5; } +.toast-error .toast-mark { color: var(--danger); } +@keyframes rise { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: none; } +} + +/* ---------- Story text polish ---------- */ + +/* Drop cap opening the story's first narrated beat. */ +.story .action.opening::first-letter { + float: left; + font-family: var(--font-display); + font-size: 3.1rem; + line-height: 0.82; + font-weight: 700; + color: var(--accent); + padding: 5px 10px 0 2px; + text-shadow: 0 0 22px var(--accent-glow); +} + +/* Ornamental separator between turns — a diamond flanked by fading rules. + Marks the beat change without the heaviness of a full
. */ +.scene-break { + display: flex; + align-items: center; + gap: 12px; + margin: 4px 0 16px; + color: var(--accent-dim); + font-size: 0.6rem; + user-select: none; +} +.scene-break::before, .scene-break::after { + content: ''; + height: 1px; + flex: 1; + background: linear-gradient(90deg, transparent, rgba(150, 119, 58, 0.4), transparent); +} + +/* Thinking indicator — three motes pulsing in sequence. */ +.thinking { + display: inline-flex; + align-items: center; + gap: 10px; + font-family: var(--font-ui); + font-size: 0.78rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--accent-dim); +} +.thinking i { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 8px var(--accent-glow); + animation: mote 1.25s ease-in-out infinite; +} +.thinking i:nth-child(2) { animation-delay: 0.18s; } +.thinking i:nth-child(3) { animation-delay: 0.36s; } +@keyframes mote { + 0%, 100% { opacity: 0.25; transform: scale(0.8); } + 45% { opacity: 1; transform: scale(1.15); } +} + +/* ---------- Scenario art picker (editor) ---------- */ + +.art-picker { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; } +.art-preview { + width: 76px; + height: 76px; + flex: none; + border-radius: 3px; + border: 1px solid var(--accent-dim); + box-shadow: inset 0 0 0 1px rgba(10, 10, 15, 0.7); + overflow: hidden; + display: grid; + place-items: center; +} +.art-preview img { width: 100%; height: 100%; object-fit: cover; display: block; } +.art-preview .glyph { font-size: 2rem; line-height: 1; } +.art-preview .mono { + font-family: var(--font-display); + font-size: 1.4rem; + font-weight: 700; + color: rgba(232, 196, 118, 0.92); +} +.art-controls { flex: 1; min-width: 220px; display: flex; flex-direction: column; gap: 9px; } +.art-buttons { display: flex; gap: 8px; flex-wrap: wrap; } +.icon-row { display: flex; gap: 5px; flex-wrap: wrap; } +.icon-row button { + padding: 3px 8px; + font-size: 1rem; + line-height: 1.2; + min-width: 34px; +} +.icon-row button.active { border-color: var(--accent); box-shadow: 0 0 10px var(--accent-glow); } +.art-hint { font-size: 0.76rem; color: var(--text-dim); line-height: 1.45; margin: 0; } + diff --git a/frontend/src/styles/tree-map.css b/frontend/src/styles/tree-map.css new file mode 100644 index 0000000..6e7533e --- /dev/null +++ b/frontend/src/styles/tree-map.css @@ -0,0 +1,159 @@ +/* ---------- The story tree, drawn (branch map overlay) ---------- */ + +.branch-map-open { + align-self: flex-start; + padding: 5px 12px; + font-size: 0.74rem; + letter-spacing: 0.08em; + color: var(--accent); + background: transparent; + border: 1px solid var(--border-bright); +} +.branch-map-open:hover { color: var(--accent-bright); border-color: var(--accent-dim); } + +/* The overlay is deliberately wider and shorter than .modal: a tree is read + across, and the number of branches decides the height. */ +.branch-map-overlay { z-index: 60; } +.branch-map { + display: flex; + flex-direction: column; + width: min(1080px, calc(100vw - 40px)); + max-height: min(760px, calc(100vh - 60px)); + background: var(--bg-panel); + border: 1px solid var(--border-bright); + border-radius: 14px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6), 0 0 40px rgba(212, 169, 78, 0.06); +} +.branch-map-header { + display: flex; + align-items: baseline; + gap: 12px; + padding: 18px 22px 12px; + border-bottom: 1px solid var(--border); +} +.branch-map-header h2 { + margin: 0; + font-family: var(--font-display); + font-size: 1.15rem; + color: var(--accent-bright); +} +.branch-map-scale { + font-size: 0.72rem; + color: var(--text-dim); + font-variant-numeric: tabular-nums; +} +.branch-map-close { + margin-left: auto; + align-self: center; + padding: 2px 8px; + color: var(--text-dim); + background: transparent; + border: 1px solid transparent; +} +.branch-map-close:hover { color: var(--accent-bright); border-color: var(--border); } + +.branch-map-canvas { + flex: 1; + min-height: 120px; + /* Height comes from the branch count, so a deep tree scrolls rather than the + diagram shrinking until nobody can read it. Width never scrolls — the map + is scaled to fit, which is the whole point of drawing it to one clock. */ + overflow-y: auto; + overflow-x: hidden; + padding: 4px 12px 8px; +} +.branch-map-svg { display: block; } + +.bm-tick line { stroke: var(--border); stroke-width: 1; stroke-dasharray: 2 6; } +.bm-tick text { + fill: var(--text-dim); + font-family: var(--font-ui); + font-size: 10px; + font-variant-numeric: tabular-nums; +} + +.bm-lane { cursor: pointer; } +.bm-hit { fill: transparent; } +.bm-lane:hover .bm-hit { fill: rgba(212, 169, 78, 0.035); } +.bm-lane.picked .bm-hit { fill: rgba(212, 169, 78, 0.07); } +.bm-lane:focus { outline: none; } +.bm-lane:focus-visible .bm-hit { fill: rgba(212, 169, 78, 0.12); } + +.bm-line { + stroke: var(--border-bright); + stroke-width: 3; + stroke-linecap: round; +} +.bm-fork { + fill: none; + stroke: var(--border-bright); + stroke-width: 2; +} +.bm-fork-dot, .bm-tip { fill: var(--border-bright); } + +.bm-lane:hover .bm-line, .bm-lane.picked .bm-line { stroke: var(--accent-dim); } +.bm-lane:hover .bm-fork, .bm-lane.picked .bm-fork { stroke: var(--accent-dim); } +.bm-lane:hover .bm-tip, .bm-lane.picked .bm-tip { fill: var(--accent); } + +/* The line being read: brighter, and its tip carries the glow so where you + are standing is findable before a word is read. */ +.bm-lane.here .bm-line { stroke: var(--accent); } +.bm-lane.here .bm-fork { stroke: var(--accent-dim); } +.bm-lane.here .bm-tip { + fill: var(--accent-bright); + filter: drop-shadow(0 0 6px var(--accent-glow)); +} +.bm-lane.here .bm-name { fill: var(--accent-bright); } + +.bm-name { + fill: var(--text); + font-family: var(--font-story); + font-size: 15px; +} +.bm-meta { + fill: var(--text-dim); + font-family: var(--font-ui); + font-size: 10.5px; + font-variant-numeric: tabular-nums; +} + +.branch-map-hint { + margin: 0; + padding: 0 22px 10px; + color: var(--text-dim); + font-size: 0.82rem; + line-height: 1.55; +} + +.branch-map-detail { + display: flex; + flex-direction: column; + gap: 6px; + padding: 14px 22px 18px; + border-top: 1px solid var(--border); +} +.branch-map-detail.here { background: rgba(212, 169, 78, 0.05); } +.bmd-head { display: flex; align-items: center; gap: 8px; } +.branch-map-detail .branch-name { font-size: 1.05rem; } +.branch-map-detail.here .branch-name { color: var(--accent-bright); } +.branch-map-detail .branch-tools button, .branch-map-detail .branch-confirm button { + padding: 4px 11px; + font-size: 0.74rem; + color: var(--text-dim); + background: transparent; + border: 1px solid var(--border); +} +.branch-map-detail .branch-tools button:hover:not(:disabled), +.branch-map-detail .branch-confirm button:hover:not(:disabled) { + color: var(--accent-bright); + border-color: var(--border-bright); +} + +@media (prefers-reduced-motion: reduce) { + .enter, .skeleton-card, .sk, .toast, .thinking i, + .story .action, .page { + animation: none !important; + } + .card.tome:hover { transform: none; } +} +