Files
interactive-story/frontend/src/index.css
T
parththakkar106andClaude Opus 5 84827f0f37 Put the tree on the screen
The pager could only step between attempts, and stepping has nothing to say
about the thing the tree exists for: taking a path the story moved past and
keeping both. Every attempt has been its own node since SP4, so a chip is a
node now, and "take this path" forks — or simply switches, when the turn is
still the tip and its attempts are leaves nobody has built on.

Beside it, a Branches panel: every line the story has taken, with where each
left its parent and where it ends, and switch, rename and delete-with-confirm.
It sits with Plot/Memory/Scripts/Insights rather than inventing a new place to
put a rail. An unnamed branch is drawn from its fork depth, never from its
position in the list — a position shifts the moment a branch above it goes.

A spatial per-node map was considered and deliberately not built. At the size
this has to be verified against it is a second windowing problem, and it can be
added later without a new endpoint, since the rail and a map read the same
GET /branches. VariantOut grows an id because a fork is addressed by the node
being taken, not by an ordinal in a group that renumbers.

Driven by hand against the 602-action fixture, which found one bug that no test
could: the panel refreshed on actions.length, and a fork swaps a 60-action
window for another 60-action window, so it went on drawing a one-branch tree
while the story was already on the second. It keys off the counter adoptWindow
bumps now.

The scroll path was driven at the same time — three prepends of ~16,200 px, the
same node holding viewport top 792 to 787, never thrown to the end. That closes
the standing gap in this project. Console clean.

396 tests, build clean, no new lint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015H5qiyiR7gtFQaoDphHZ3g
2026-08-18 19:14:07 +05:30

2374 lines
68 KiB
CSS

: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;
--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 attempts at one AI beat. Stays quiet until hovered — it's a footnote on
the message, not part of the prose. */
.attempts {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
margin-top: 6px;
font-family: var(--font-ui);
opacity: 0.45;
transition: opacity 0.15s;
}
.story .action:hover .attempts,
.attempts:focus-within { opacity: 1; }
.attempt-chip {
padding: 3px 10px;
font-size: 0.73rem;
line-height: 1.5;
border-radius: 999px;
color: var(--text-dim);
background: transparent;
border: 1px solid var(--border);
}
.attempt-chip:hover:not(:disabled) { color: var(--text); border-color: var(--border-bright); }
.attempt-chip[aria-pressed="true"] {
color: var(--bg);
background: var(--accent);
border-color: var(--accent);
font-weight: 600;
}
.attempt-chip:disabled { opacity: 0.35; cursor: default; }
/* Dashed until hovered: taking a path the story moved past creates a branch,
so it should not look like the same weight of click as browsing one. */
.take-path {
padding: 3px 10px;
font-size: 0.73rem;
line-height: 1.5;
color: var(--accent);
background: transparent;
border: 1px dashed var(--accent-dim);
}
.take-path:hover:not(:disabled) { color: var(--accent-bright); border-style: solid; }
.take-path:disabled { opacity: 0.35; cursor: default; }
.attempt-note {
margin-left: 2px;
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 `<input>`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;
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)); }
.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 <hr>. */
.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; }
@media (prefers-reduced-motion: reduce) {
.enter, .skeleton-card, .sk, .toast, .thinking i,
.story .action, .page {
animation: none !important;
}
.card.tome:hover { transform: none; }
}
/* ============================================================
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; }
}