M8: the browser becomes the storyteller
The interface was AI-DnD's with this product's features bolted into it. The
navigation read Home · Adventures · Scenarios · Settings · AI Chat; starting a
story meant first picking a *world*, and making a world meant a JSON stat-schema
form, a story-card table and an art picker. The play screen had a Branches tab.
The input had three modes. Sixteen of the sixteen controls on a two-turn story
had no accessible name — they were single glyphs with a tooltip.
All of that was measured in a real browser before anything was changed, and the
measurements are in planning/reports/M8-IMPLEMENTATION-REPORT.md §C. Almost
nothing underneath was wrong: the play loop, the history controls, the takes,
the Save Points, the state correction and the knowledge library all worked. What
was wrong was what a reader was asked to understand in order to use them.
So the shape now is one entry point and one screen:
Campaigns -> Campaign -> Story
State · Knowledge · Context · Save Points · Settings
Everything that is not the story lives in a panel that starts closed. The
top navigation bar is hidden on the story screen entirely, because on that one
screen the story is the interface.
Play is one natural-language field. An action and a piece of quoted dialogue are
both just what the reader wrote, and B01/B02 confirmed against a real narrator
that the model reads the quotes without being told which kind of turn it is.
What survives from the old Story mode is a Story direction toggle, which is not
a fourth mode: it changes who is being spoken to, not what kind of action is
taken, and the box is visibly marked while it is on.
Branch, fork, node, merge and head appear nowhere a reader can see them. The
branch panel and the tree overlay are gone from the browser. The mechanism is
untouched — takes, divergence, retained futures and Save Points all still work,
and their endpoints are still tested. This is a decision about what a reader is
asked to understand, not a reduction of what the product can do.
The two defects worth the space:
A player action is stored with AI Dungeon's "> You " prefix. That was right when
the Do mode asked for a bare verb phrase. With one field the spec tells the
reader to write "I enter the tavern", and the result was "> You I enter the
tavern." — in the transcript, in the replayed history, and therefore in the
narration, where a small model imitates it and writes "You I thank her". M8's
own design surfaced it, so M8 fixed it: the prefix is added only when the reader
has not already written a subject. The ">" marker, which is what actually
identifies a player turn in the prompt, is unchanged in every case.
And a stale `.input-bar { display: flex }` in play.css overrode the new
composer, because that sheet is imported after the new one. The direction row
and the input row laid out side by side and the box was unusably narrow. Found
by opening the product in a browser, not by reading the CSS — which is the
argument for having done that first.
Failures now have the taxonomy the spec asked for rather than one toast: model,
generation, state, knowledge, server, each with the thing to do about it. A
failed turn leaves the reader's words in the box and says so. The classification
reads backend strings, so it is a fallback ladder rather than a lookup — an
unrecognised message still classifies, still shows the server's own words and
still offers Retry.
`Settings.model` could be empty with nothing saying so until the first turn
failed with a provider error. The header now reports Ollama in five states, and
an unconfigured or missing model offers the models actually installed on the
endpoint, from the connection test that already knew them. Nothing is chosen
automatically: an endpoint's first model may be an embedding model, which cannot
narrate at all.
Narrator prose is rendered as safe Markdown — headings, emphasis, lists,
blockquotes, code. The safety is structural rather than filtered: every node is
a React element built from parsed text, and there is no dangerouslySetInnerHTML
in the file. A sanitizer is not needed to make markup safe if markup is never
produced from input. Link schemes are checked with the URL parser rather than a
pattern, because the bypasses are all in the parsing. A remote image is a
placeholder naming the blocked address; the knowledge and context panels
deliberately do not use this renderer at all, because they exist to show a
reader exactly what is in their file.
Backend, and only what the browser could not otherwise reach:
AdventureCreate.opening a start action could only come from a Scenario, so
every campaign made in the new setup flow opened on
a blank page. Same node, same code path.
canon_rules campaign_canon has been the highest authority in a
campaign since M5, read by the prompt builder and
the state validator, and had no API at all — a
fixture had to write it with SQL.
a 401 and a 429 message the last user-facing text describing a hosted
deployment. One told the reader to check an API key
that has not existed since M2.
No schema change and no migration: proved by building a database with a server
running the M7 commit's own code and opening it with this one.
The project had no frontend tests. It has 132 now, across ten files, running
in about six seconds — the enabled state of every history control, the take
selector, the confirmations, the panels, the five model states, the failure
taxonomy, the focus trap, accessibility, and that the reserved dictation control
never touches the microphone. Writing them found a real defect: the focus trap
filtered candidates with offsetParent, which is null inside the fixed-position
ancestor the dialog has and which jsdom never computes — it would have behaved
differently in the tests from the browser.
They do not replace the real-browser runs, and both kinds of evidence are in the
report. The browser suites drive the production build served by the real backend
with a real local narrator, including a genuine process restart.
A verification pass over all of it then found three more, each by driving the
product rather than reading it:
Stepping between alternate takes did nothing. The pager asked whether a take
lived on another line by comparing `target.branch_id !== action.branch_id`, and
`ActionOut` has never carried `branch_id` — so the comparison was permanently
`number !== undefined`, always true, and every step took the branch-switch path.
For two takes of an ordinary retry, which share a line until one is written
below, that meant switching to the line already being read: the same window came
back and nothing moved. D07 is a required v1 acceptance test. The fix needed no
new field — the variants list already carries every attempt's branch and marks
the live one.
The first regression test for that passed against the broken code, because its
fixture gave the action a `branch_id` the real payload never sends. That is the
exact failure M7's review was about, so the fixture was corrected, the tests were
re-run against the reverted code and failed for the right reason, and the
fixture now carries a docstring saying why the field must never come back.
And the knowledge panel pointed readers at an "embedding model" while the
setting is called "Model for meaning-based search" — a reader sent looking for a
field that does not exist by that name.
Campaign canon was measured rather than assumed. Editing it after play is a
configuration change: every turn already played keeps the canon it was actually
given, in its own context snapshot, and the accepted story, the state document
and the state audit log are byte-identical across an edit. It is not routed
through M5's state audit, because canon is not narrative state and doing so
would create the second representation the spec forbids. What the editor does
now is say so, once a campaign has moments.
`BROWSER-UX-SPEC.md` §38 asked for a "Show Hidden Story State" toggle. There is
no hidden story state — a secret lives in a narrator-only knowledge source and
never enters the state document. The section is rewritten to require what it
actually meant: ordinary surfaces must not carry narrator-only information,
advanced inspection must withhold it by default behind an explicit warned
choice, and no second store may be invented to give a toggle something to
reveal. The protection is stricter than before, not weaker.
Closeout. An independent review returned M8 IMPLEMENTATION: PASS subject to
evidence and documentation cleanup, and this commit carries that cleanup:
The report named two frontend bundles as the artifact behind its acceptance
evidence. The saved run logs settle it. index-Ii-lARp9.js, built at 18:53:02
from this tree, is the one final frozen artifact behind all 157 browser checks;
index-C6E5Uvtu.js is superseded — it predates the D09 fix and its acceptance
suite ended 54/55 on exactly that defect. No tracked file under backend/app or
frontend/src has a modification time after the freeze, so the whole final
campaign describes one build. §P sets the two side by side.
Finding 14 — the app budgets 16,384 prompt tokens while an Ollama that sees no
VRAM enforces 4,096 — is resolved operationally, with no application change.
The OpenAI-compatible endpoint this app speaks accepts num_ctx and ignores it,
and reloads the model at its own default, so a native call cannot prime it
either. A model derived with POST /api/create carries the parameter, is honoured
through the app's own OpenAI-compatible path, and appears in /v1/models — which
is the listing the Settings model picker already reads. Measured end to end.
The procedure is in DEVELOPMENT.md; nothing in the repository depends on any
particular derived model existing. Adding provider code to work around this was
declined deliberately: it would mean either a second native request path,
against ADR 011, or a parameter the endpoint provably ignores.
The §38 rewrite is ratified as a requirement clarification aligned with the
implemented architecture, and the spec gains the clause finding 3 was really
about: withheld material must be absent from the rendered DOM, not merely
collapsed in it.
The report's §U carries the M9 handoff — what a portable campaign has to include,
whether historical context snapshots belong in the bundle, what happens to
inherited story cards, and that a restored campaign may meet a different context
window than the one that wrote it. None of it is implemented here.
Final: backend 950 passed / 14 skipped; frontend 132 passed; lint, production
build and Docker build clean; 157 browser checks across six suites, zero
failures. M8 is implemented, verified, reviewed and accepted (2026-09-06).
M9 has not been started.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HdaXiFbscatQaLS7dJk6b
This commit is contained in:
co-authored by
Claude Opus 5
parent
480414efe0
commit
1ce9972760
@@ -1,287 +0,0 @@
|
||||
/* ---------- 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;
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
/* ---------- 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;
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/* ---------- 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); }
|
||||
}
|
||||
|
||||
@@ -1,97 +0,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; }
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/* The context inspector, and the knowledge library's list.
|
||||
*
|
||||
* §55's rule shapes both: readable summaries first, raw text last and folded
|
||||
* away. Everything here is UI-font and small — this is the layer under the
|
||||
* story, and it should not compete with it.
|
||||
*/
|
||||
|
||||
.context-panel { display: flex; flex-direction: column; gap: 14px; font-size: 0.82rem; }
|
||||
|
||||
.ctx-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.ctx-mode button { padding: 3px 9px; font-size: 0.72rem; }
|
||||
|
||||
.ctx-tokens {
|
||||
padding: 11px 13px;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
}
|
||||
.ctx-tokens.over { border-color: var(--danger); }
|
||||
.ctx-token-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 2px 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.ctx-warn { margin: 8px 0 0; color: var(--danger); font-size: 0.76rem; }
|
||||
|
||||
.token-bar {
|
||||
display: flex;
|
||||
height: 6px;
|
||||
margin-top: 9px;
|
||||
overflow: hidden;
|
||||
border-radius: 3px;
|
||||
background: var(--bg);
|
||||
}
|
||||
.token-bar-slice { height: 100%; }
|
||||
.slice-0 { background: var(--accent); }
|
||||
.slice-1 { background: var(--player); }
|
||||
.slice-2 { background: #b39ddb; }
|
||||
.slice-3 { background: var(--accent-dim); }
|
||||
.slice-4 { background: #6f9e8c; }
|
||||
.slice-5 { background: #c48a6a; }
|
||||
.slice-6 { background: #7c86b8; }
|
||||
.slice-7 { background: var(--border-bright); }
|
||||
|
||||
.ctx-block {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
.ctx-block > summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 9px 12px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
.ctx-block > summary::-webkit-details-marker { display: none; }
|
||||
.ctx-block > summary::before {
|
||||
content: '▸';
|
||||
margin-right: 7px;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
.ctx-block[open] > summary::before { content: '▾'; }
|
||||
.ctx-block-title { flex: 1; font-weight: 600; }
|
||||
.ctx-block-count {
|
||||
padding: 0 7px;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.ctx-block-body { padding: 0 12px 12px; }
|
||||
.ctx-block-body > p:first-child { margin-top: 0; }
|
||||
|
||||
.ctx-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
|
||||
.ctx-row {
|
||||
padding: 9px 11px;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.ctx-row-head { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; margin-bottom: 4px; }
|
||||
.ctx-row-meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.ctx-row-text, .ctx-row-text-plain {
|
||||
margin: 7px 0 0;
|
||||
padding: 8px 10px;
|
||||
max-height: 16em;
|
||||
overflow: auto;
|
||||
font-family: var(--font-story);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
background: var(--bg);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.ctx-row-withheld {
|
||||
margin: 7px 0 0;
|
||||
padding: 8px 10px;
|
||||
font-style: italic;
|
||||
color: var(--text-dim);
|
||||
background: var(--bg);
|
||||
border: 1px dashed var(--border-bright);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.ctx-source-link { font-weight: 600; }
|
||||
.ctx-foot { margin: 9px 0 0; font-size: 0.74rem; }
|
||||
.ctx-note { margin: 9px 0 0; font-size: 0.76rem; color: var(--accent-dim); line-height: 1.55; }
|
||||
|
||||
/* §19. Deliberately not styled as a quiet checkbox: it reveals story secrets,
|
||||
and it should look like it means something. */
|
||||
.reveal-toggle {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 11px;
|
||||
padding: 8px 10px;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text-dim);
|
||||
background: var(--bg-input);
|
||||
border: 1px dashed var(--accent-dim);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.reveal-toggle input { margin-top: 2px; }
|
||||
|
||||
.ctx-settings { display: grid; grid-template-columns: auto 1fr; gap: 3px 14px; margin: 0; }
|
||||
.ctx-settings dt { color: var(--text-dim); }
|
||||
.ctx-settings dd { margin: 0; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.ctx-raw-section { margin-top: 10px; }
|
||||
.ctx-raw-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 5px 0;
|
||||
font-size: 0.72rem;
|
||||
color: var(--accent-dim);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.ctx-raw-section pre {
|
||||
margin: 6px 0 0;
|
||||
padding: 9px 11px;
|
||||
max-height: 22em;
|
||||
overflow: auto;
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
background: var(--bg-input);
|
||||
border-radius: 5px;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/* The settings screen's request log. What remains of `auth.css`, whose
|
||||
log-in, sign-up and "update from scenario" rules went with the surfaces
|
||||
they styled (M8 §26). */
|
||||
|
||||
/* ---------- 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;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/* Modal dialogs, and the class tags shared by the knowledge and context panels. */
|
||||
|
||||
.dialog-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(4, 4, 8, 0.72);
|
||||
backdrop-filter: blur(3px);
|
||||
animation: dialog-fade 0.15s ease both;
|
||||
}
|
||||
@keyframes dialog-fade { from { opacity: 0; } }
|
||||
|
||||
.dialog {
|
||||
width: min(460px, 100%);
|
||||
max-height: 86vh;
|
||||
overflow-y: auto;
|
||||
padding: 22px 24px;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-bright);
|
||||
border-radius: 11px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
.dialog:focus { outline: none; }
|
||||
.dialog-title { margin: 0 0 12px; font-size: 1.05rem; }
|
||||
.dialog-body { font-size: 0.87rem; line-height: 1.65; color: var(--text-dim); }
|
||||
.dialog-body p { margin: 0 0 10px; }
|
||||
.dialog-body p:last-child { margin-bottom: 0; }
|
||||
.dialog-field { display: flex; flex-direction: column; gap: 5px; margin-top: 16px; }
|
||||
.dialog-field span { font-size: 0.76rem; color: var(--text-dim); }
|
||||
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
|
||||
|
||||
.external-href {
|
||||
padding: 8px 10px;
|
||||
overflow-wrap: anywhere;
|
||||
background: var(--bg-input);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ---------- Classification tags ---------- *
|
||||
* One definition, used by the knowledge library and the context inspector, so
|
||||
* a class looks the same wherever a reader meets it (§48). */
|
||||
|
||||
.class-tag {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
border-radius: 10px;
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
.class-canon { color: var(--accent-bright); }
|
||||
.class-reference { color: var(--player); }
|
||||
.class-inspiration { color: #b39ddb; }
|
||||
|
||||
.hidden-tag {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--bg);
|
||||
background: var(--accent-dim);
|
||||
border-radius: 10px;
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
/* ---------- 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; }
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
/* The campaign library, the setup form, and the model status badge.
|
||||
*
|
||||
* Everything above a campaign. Deliberately plain: this screen is passed
|
||||
* through on the way to a story, and the thing a returning reader is looking
|
||||
* for is the title of the story they were in.
|
||||
*/
|
||||
|
||||
.page {
|
||||
width: min(980px, 100%);
|
||||
margin: 0 auto;
|
||||
padding: 30px 24px 80px;
|
||||
}
|
||||
.page.setup, .page.settings { width: min(720px, 100%); }
|
||||
|
||||
/* ---------- Library ---------- */
|
||||
|
||||
.library-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
.library-head h1 { margin: 0 0 4px; font-size: 1.5rem; }
|
||||
.library-sub { margin: 0; color: var(--text-dim); font-size: 0.86rem; }
|
||||
.library-actions { display: flex; gap: 10px; }
|
||||
|
||||
.library-empty {
|
||||
max-width: 46ch;
|
||||
margin: 40px auto;
|
||||
text-align: center;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.library-empty h2 { margin: 0 0 10px; color: var(--text); font-size: 1.15rem; }
|
||||
.library-empty p { line-height: 1.65; margin-bottom: 20px; }
|
||||
|
||||
.campaign-grid {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.campaign-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
padding: 16px 18px;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9px;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.campaign-card:hover { border-color: var(--border-bright); }
|
||||
.campaign-title { margin: 0; font-size: 1.05rem; font-weight: 600; }
|
||||
.campaign-title a { color: var(--text); }
|
||||
.campaign-title a:hover { color: var(--accent-bright); }
|
||||
.campaign-meta {
|
||||
margin: 0;
|
||||
font-size: 0.74rem;
|
||||
color: var(--text-dim);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.campaign-snippet {
|
||||
flex: 1;
|
||||
margin: 4px 0 8px;
|
||||
font-family: var(--font-story);
|
||||
font-size: 0.94rem;
|
||||
line-height: 1.55;
|
||||
color: var(--text-dim);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.campaign-snippet.muted { font-style: italic; opacity: 0.7; }
|
||||
.campaign-tools { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.campaign-tools .button,
|
||||
.campaign-tools button {
|
||||
padding: 4px 11px;
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
/* ---------- Setup ---------- */
|
||||
|
||||
.setup h1 { margin: 0 0 6px; font-size: 1.5rem; }
|
||||
.setup-lede { margin: 0 0 26px; color: var(--text-dim); font-size: 0.9rem; line-height: 1.6; }
|
||||
.setup-section { margin-bottom: 28px; }
|
||||
.setup-section h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-dim);
|
||||
}
|
||||
.setup-actions { display: flex; gap: 10px; margin-top: 8px; }
|
||||
|
||||
.field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 14px; }
|
||||
.field > label {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.field-row { display: flex; gap: 14px; flex-wrap: wrap; }
|
||||
.field-row > .field { flex: 1; min-width: 190px; }
|
||||
.field-hint { margin: 2px 0 0; font-size: 0.75rem; line-height: 1.55; color: var(--text-dim); }
|
||||
|
||||
/* ---------- Model status ---------- */
|
||||
|
||||
.model-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 4px 11px;
|
||||
font-size: 0.73rem;
|
||||
color: var(--text-dim);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.model-badge:hover { border-color: var(--border-bright); color: var(--text); }
|
||||
.model-badge-dot {
|
||||
width: 6px; height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
}
|
||||
.model-badge.bad { color: var(--danger); border-color: var(--danger); }
|
||||
.model-badge.bad .model-badge-dot { background: var(--danger); }
|
||||
.model-badge.checking .model-badge-dot { background: var(--text-dim); }
|
||||
.model-badge-model { color: var(--text-dim); opacity: 0.8; }
|
||||
|
||||
.notice {
|
||||
margin: 0 0 20px;
|
||||
padding: 14px 16px;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-bright);
|
||||
border-left: 3px solid var(--accent);
|
||||
border-radius: 7px;
|
||||
}
|
||||
.notice.error { border-left-color: var(--danger); }
|
||||
.notice strong { display: block; margin-bottom: 6px; }
|
||||
.notice p { margin: 0 0 8px; color: var(--text-dim); }
|
||||
.notice-detail { font-size: 0.8rem; color: var(--text-dim); }
|
||||
.notice-detail.error { color: var(--danger); }
|
||||
.notice-steps { margin: 8px 0; padding-left: 1.3em; color: var(--text-dim); }
|
||||
.notice-steps li { margin-bottom: 5px; }
|
||||
.notice-foot { margin: 10px 0 0 !important; }
|
||||
.notice code {
|
||||
padding: 1px 5px;
|
||||
font-size: 0.86em;
|
||||
background: var(--bg-input);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.model-choices { list-style: none; display: flex; flex-wrap: wrap; gap: 7px; margin: 10px 0; padding: 0; }
|
||||
.model-choices button {
|
||||
padding: 5px 12px;
|
||||
font-size: 0.8rem;
|
||||
font-family: var(--font-ui);
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border-bright);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.model-choices button:hover { border-color: var(--accent); color: var(--accent-bright); }
|
||||
|
||||
.local-only {
|
||||
margin-bottom: 24px;
|
||||
padding: 14px 16px;
|
||||
font-size: 0.85rem;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
}
|
||||
.local-only strong { display: block; margin-bottom: 6px; color: var(--accent-bright); }
|
||||
.local-only p { margin: 0; color: var(--text-dim); line-height: 1.6; }
|
||||
|
||||
.settings-section { margin-bottom: 34px; }
|
||||
.settings-section h2 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-dim);
|
||||
}
|
||||
.panel-actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
|
||||
.test-result { margin-top: 12px; font-size: 0.83rem; }
|
||||
.test-result.ok { color: var(--accent); }
|
||||
.test-result.bad { color: var(--danger); }
|
||||
|
||||
.advanced-block {
|
||||
margin-top: 14px;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
}
|
||||
.advanced-block > summary {
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.advanced-block > summary:hover { color: var(--text); }
|
||||
.advanced-block h4 { margin: 10px 0 4px; font-size: 0.74rem; color: var(--text-dim); }
|
||||
.advanced-block pre {
|
||||
max-height: 16em;
|
||||
overflow: auto;
|
||||
padding: 8px 10px;
|
||||
font-size: 0.74rem;
|
||||
background: var(--bg-input);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ---------- Utility ---------- */
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
margin: -1px; padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
/* Visible the moment it is focused, which is the whole point of it. */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
z-index: 100;
|
||||
padding: 10px 16px;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 0 0 6px 0;
|
||||
}
|
||||
.skip-link:focus { left: 0; top: 0; }
|
||||
|
||||
.dim { color: var(--text-dim); }
|
||||
.linklike {
|
||||
padding: 0;
|
||||
color: var(--accent);
|
||||
background: none;
|
||||
border: none;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
text-decoration: underline dotted;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.linklike:hover { color: var(--accent-bright); }
|
||||
|
||||
/* A setting whose change is worth thinking about before it is made. Amber
|
||||
rather than red: nothing is destroyed, but the reader should know what the
|
||||
change does and does not reach. */
|
||||
.field-warning {
|
||||
margin: 8px 0 0;
|
||||
padding: 9px 11px;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-dim);
|
||||
background: var(--bg-input);
|
||||
border-left: 2px solid var(--accent-dim);
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
.field-warning strong { color: var(--accent-bright); }
|
||||
+12
-184
@@ -1,36 +1,8 @@
|
||||
/* ---------- 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. */
|
||||
@@ -56,23 +28,12 @@
|
||||
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);
|
||||
@@ -104,27 +65,6 @@
|
||||
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.
|
||||
|
||||
@@ -140,7 +80,7 @@
|
||||
opacity: 0.45;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.story .action:hover .take-pager,
|
||||
.turn:hover .take-pager,
|
||||
.take-pager:focus-within { opacity: 1; }
|
||||
.take-pager button {
|
||||
width: 20px;
|
||||
@@ -174,92 +114,17 @@
|
||||
color: var(--accent-dim);
|
||||
}
|
||||
|
||||
/* ---------- Branches panel (Phase 14, SP7) ---------- */
|
||||
/* ---------- Shared panel furniture ---------- */
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
/* Save Points (M4). Deliberately the same furniture as the branch panel: a Save
|
||||
Point is a name for a position, and it should not look like a different
|
||||
species of thing from the lines it names positions on. The confirmations
|
||||
borrow the branch panel's shape but not its danger colouring — restoring
|
||||
destroys nothing, and only deletion is red. */
|
||||
/* Save Points (M4). A Save Point is a name for a position in the story, and
|
||||
the confirmations are shaped so that restoring does not read as dangerous —
|
||||
it destroys nothing. Only deletion is red. */
|
||||
.save-point-panel { display: flex; flex-direction: column; gap: 14px; }
|
||||
.save-point-new { display: flex; flex-direction: column; gap: 7px; }
|
||||
.save-point-new label {
|
||||
@@ -366,49 +231,12 @@
|
||||
}
|
||||
.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); }
|
||||
/* The composer, the mode selector and the sticky control bar that used to
|
||||
live here are gone: M8 replaced them with one input plus a direction
|
||||
toggle, styled in `story.css`. The `.input-bar { display: flex }` left
|
||||
behind here overrode the new layout, because this sheet is imported after
|
||||
that one — which is exactly the hazard the import order comment warns of. */
|
||||
|
||||
.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; }
|
||||
|
||||
/* Story State (M5). The same furniture as the other panels: what the campaign
|
||||
believes is not a different species of thing from the lines it believes them
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
/* ---- 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;
|
||||
/* 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. Text may break
|
||||
mid-word as a last resort, which is what keeps a chip's minimum width
|
||||
small enough for the flex row to shrink it. */
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.chg-val { overflow-wrap: anywhere; }
|
||||
/* Only a signed number is held on one line, so the sign never ends up on a
|
||||
line of its own. The other values a chip can carry are phrases — a refusal
|
||||
reason, "no change — at its limit", or the whole new value of a free-text
|
||||
stat — and holding those together set the chip's minimum width to the width
|
||||
of the phrase, which is what pushed a chip out of the message on a phone. */
|
||||
.chg-num { 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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
/* The story screen: layout, transcript, composer.
|
||||
*
|
||||
* `BROWSER-UX-SPEC.md` §82-84 and §98. Three rules do most of the work here:
|
||||
*
|
||||
* §82 prose gets a readable measure and does not stretch across a wide
|
||||
* monitor, whatever the window is doing;
|
||||
* §83 no card chrome around every message — the story reads as prose, and a
|
||||
* player turn is set apart by indentation and colour rather than a box;
|
||||
* §84 no metadata on screen, so the controls on a turn stay invisible until
|
||||
* the pointer or the keyboard reaches that turn.
|
||||
*
|
||||
* The transcript scrolls the page rather than an inner element. That keeps the
|
||||
* browser's own find-in-page, scroll restoration and keyboard paging working,
|
||||
* which an inner scroller quietly breaks.
|
||||
*/
|
||||
|
||||
/* ---------- Layout ---------- */
|
||||
|
||||
.play {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.play-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 10px 20px;
|
||||
/* Opaque, not glass. The first browser pass had story text reading straight
|
||||
through a translucent bar; a header the story shows through is not a
|
||||
header. */
|
||||
background: var(--bg-panel);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.play-title { display: flex; align-items: baseline; gap: 14px; min-width: 0; flex: 1; }
|
||||
.play-title h1 {
|
||||
margin: 0;
|
||||
font-size: 1.02rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.play-title .back {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.play-title .back:hover { color: var(--accent); }
|
||||
.play-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-tabs { display: flex; gap: 2px; }
|
||||
.panel-tabs button {
|
||||
padding: 5px 11px;
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-dim);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.panel-tabs button:hover { color: var(--text); border-color: var(--border); }
|
||||
.panel-tabs button.active {
|
||||
color: var(--accent-bright);
|
||||
border-color: var(--border-bright);
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
.play-main { display: flex; flex: 1; align-items: flex-start; min-height: 0; }
|
||||
.play-story {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* Fills the viewport below the header, so the composer sits at the bottom of
|
||||
a short story rather than immediately under its last line. */
|
||||
min-height: calc(100vh - 52px);
|
||||
}
|
||||
|
||||
/* ---------- Transcript ---------- */
|
||||
|
||||
.story {
|
||||
/* §82. The measure is the point: prose is read, not scanned, and 66ch is
|
||||
about the width a line can be before the eye loses the next one. */
|
||||
width: min(66ch, 100%);
|
||||
margin: 0 auto;
|
||||
/* The bottom padding clears the sticky composer. Enough to scroll the last
|
||||
line clear of it, not so much that a short story opens on a void. */
|
||||
padding: 36px 24px 40px;
|
||||
flex: 1;
|
||||
font-family: var(--font-story);
|
||||
font-size: 1.14rem;
|
||||
line-height: 1.72;
|
||||
}
|
||||
.story-empty { color: var(--text-dim); font-style: italic; }
|
||||
|
||||
.turn {
|
||||
position: relative;
|
||||
margin: 0 0 1.35em;
|
||||
animation: turn-in 0.35s ease both;
|
||||
}
|
||||
@keyframes turn-in { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; } }
|
||||
|
||||
.turn-text > *:first-child { margin-top: 0; }
|
||||
.turn-text > *:last-child { margin-bottom: 0; }
|
||||
.turn-text p { margin: 0 0 0.75em; }
|
||||
|
||||
/* §83. A player turn is the reader's own voice: indented, lighter, in the
|
||||
cooler of the two accents. No box, no avatar, no timestamp. */
|
||||
.turn-player .turn-text {
|
||||
padding-left: 1.1em;
|
||||
border-left: 2px solid var(--player);
|
||||
color: var(--player);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.turn-narrator .turn-text { color: var(--text); }
|
||||
|
||||
/* The opening beat gets the drop cap, and only that one. */
|
||||
.turn-opening .turn-text > p:first-of-type::first-letter {
|
||||
float: left;
|
||||
font-family: var(--font-display);
|
||||
font-size: 3.1em;
|
||||
line-height: 0.82;
|
||||
padding: 0.06em 0.09em 0 0;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.scene-break {
|
||||
margin: 1.8em 0;
|
||||
text-align: center;
|
||||
color: var(--border-bright);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.5em;
|
||||
}
|
||||
|
||||
.story .cursor {
|
||||
display: inline-block;
|
||||
margin-left: 1px;
|
||||
color: var(--accent);
|
||||
animation: blink 1s steps(2) infinite;
|
||||
}
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
|
||||
.story .reasoning {
|
||||
margin-bottom: 0.5em;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.story .reasoning summary {
|
||||
cursor: pointer;
|
||||
color: var(--text-dim);
|
||||
opacity: 0.7;
|
||||
}
|
||||
.story .reasoning summary:hover { opacity: 1; }
|
||||
.story .reasoning .reasoning-text {
|
||||
margin-top: 5px;
|
||||
padding-left: 10px;
|
||||
border-left: 1px solid var(--border);
|
||||
color: var(--text-dim);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* §84. Controls stay out of the way until the turn is reached — but they are
|
||||
real buttons in the tab order, so `:focus-within` has to reveal them or a
|
||||
keyboard reader would be operating something they cannot see. */
|
||||
.turn-tools {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 6px;
|
||||
font-family: var(--font-ui);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.turn:hover .turn-tools,
|
||||
.turn-tools:focus-within { opacity: 1; }
|
||||
.turn-tools button {
|
||||
padding: 2px 8px;
|
||||
font-size: 0.71rem;
|
||||
color: var(--text-dim);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.turn-tools button:hover { color: var(--text); border-color: var(--border-bright); }
|
||||
|
||||
.turn-edit { margin: 0 0 1.35em; }
|
||||
.turn-edit textarea {
|
||||
width: 100%;
|
||||
font-family: var(--font-story);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.edit-buttons { display: flex; gap: 8px; margin-top: 8px; }
|
||||
.edit-hint {
|
||||
margin: 8px 0 0;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.story-earlier { margin-bottom: 1.6em; text-align: center; font-family: var(--font-ui); }
|
||||
.story-earlier button {
|
||||
padding: 4px 14px;
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-dim);
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
}
|
||||
.story-earlier button:hover { color: var(--accent-bright); border-color: var(--border-bright); }
|
||||
|
||||
.thinking {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.thinking i {
|
||||
width: 5px; height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
animation: pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
.thinking i:nth-child(2) { animation-delay: 0.15s; }
|
||||
.thinking i:nth-child(3) { animation-delay: 0.3s; }
|
||||
@keyframes pulse { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }
|
||||
|
||||
/* ---------- Markdown in story prose ---------- */
|
||||
|
||||
.md-heading { font-family: var(--font-display); margin: 1.1em 0 0.4em; }
|
||||
.md-list { margin: 0 0 0.75em; padding-left: 1.4em; }
|
||||
.md-quote {
|
||||
margin: 0 0 0.75em;
|
||||
padding-left: 0.9em;
|
||||
border-left: 2px solid var(--border-bright);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.md-code {
|
||||
margin: 0 0 0.75em;
|
||||
padding: 10px 12px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.85rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.turn-text code {
|
||||
padding: 1px 4px;
|
||||
font-size: 0.88em;
|
||||
background: var(--bg-input);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.md-link { text-decoration: underline dotted; text-underline-offset: 3px; }
|
||||
/* §75: the address is shown, and nothing is fetched. */
|
||||
.md-image-blocked,
|
||||
.md-link-blocked {
|
||||
display: inline-block;
|
||||
padding: 1px 7px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-dim);
|
||||
background: var(--bg-input);
|
||||
border: 1px dashed var(--border-bright);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ---------- Composer ---------- */
|
||||
|
||||
.composer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 15;
|
||||
width: min(66ch, 100%);
|
||||
margin: 0 auto;
|
||||
padding: 10px 24px calc(16px + env(keyboard-inset-height, 0px));
|
||||
background: linear-gradient(to top, var(--bg) 62%, transparent);
|
||||
}
|
||||
|
||||
.story-controls { display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; }
|
||||
.story-controls button {
|
||||
padding: 5px 13px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-dim);
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.story-controls button:hover:not(:disabled) {
|
||||
color: var(--accent-bright);
|
||||
border-color: var(--border-bright);
|
||||
}
|
||||
.story-controls button:disabled { opacity: 0.35; cursor: default; }
|
||||
|
||||
.input-bar {
|
||||
border: 1px solid var(--border-bright);
|
||||
border-radius: 9px;
|
||||
background: var(--bg-panel);
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.input-bar:focus-within { border-color: var(--accent-dim); box-shadow: 0 0 0 3px var(--accent-glow); }
|
||||
/* §13: while direction is on, the box must not look like the story box. */
|
||||
.input-bar.directing { border-color: var(--accent); background: rgba(212, 169, 78, 0.06); }
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 7px 12px 0;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
.direction-toggle { display: flex; align-items: center; gap: 6px; color: var(--text-dim); cursor: pointer; }
|
||||
.direction-toggle input { margin: 0; }
|
||||
.input-bar.directing .direction-toggle { color: var(--accent-bright); }
|
||||
.direction-hint { color: var(--text-dim); font-style: italic; }
|
||||
|
||||
.input-main { display: flex; align-items: flex-end; gap: 8px; padding: 4px 10px 10px; }
|
||||
.input-main textarea {
|
||||
flex: 1;
|
||||
/* forms.css gives every textarea a 70px floor, which is right for a form
|
||||
field and wrong for a composer that starts at one line and grows with what
|
||||
is typed. */
|
||||
min-height: 0;
|
||||
max-height: 9em;
|
||||
padding: 6px 4px;
|
||||
font-family: var(--font-story);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.55;
|
||||
color: var(--text);
|
||||
background: transparent;
|
||||
border: none;
|
||||
resize: none;
|
||||
}
|
||||
.input-main textarea:focus { outline: none; }
|
||||
|
||||
/* Reserved for a later version. Disabled, and visibly so. */
|
||||
.dictate-reserved {
|
||||
padding: 5px 9px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-dim);
|
||||
background: transparent;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 6px;
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ---------- Failure ---------- */
|
||||
|
||||
.failure {
|
||||
width: min(66ch, 100%);
|
||||
margin: 0 auto 12px;
|
||||
padding: 12px 16px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 0.84rem;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--danger);
|
||||
border-left-width: 3px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
.failure-knowledge, .failure-state { border-color: var(--accent-dim); }
|
||||
.failure-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||
.failure-head strong { flex: 1; }
|
||||
.failure-icon { color: var(--danger); }
|
||||
.failure-close {
|
||||
padding: 0 5px;
|
||||
color: var(--text-dim);
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
.failure-hint, .failure-kept { margin: 0 0 6px; color: var(--text-dim); line-height: 1.55; }
|
||||
.failure-kept { font-style: italic; }
|
||||
.failure details { margin-bottom: 6px; }
|
||||
.failure summary { cursor: pointer; color: var(--text-dim); font-size: 0.78rem; }
|
||||
.failure pre, .failure blockquote {
|
||||
margin: 6px 0 0;
|
||||
padding: 8px 10px;
|
||||
max-height: 12em;
|
||||
overflow: auto;
|
||||
font-size: 0.76rem;
|
||||
white-space: pre-wrap;
|
||||
background: var(--bg-input);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.failure-actions { display: flex; gap: 8px; margin-top: 8px; }
|
||||
|
||||
/* The model-setup guidance, when it appears above the composer. Same measure as
|
||||
the prose, so it reads as part of the column rather than a banner across it. */
|
||||
.story-blocked {
|
||||
width: min(66ch, 100%);
|
||||
margin: 0 auto 12px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
@@ -1,475 +0,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; }
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
/* ---------- 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; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user