Split index.css into eighteen section files

`frontend/src/index.css` held 2865 lines. It is now an import list, and each
section is its own file under `frontend/src/styles/`.

The order is unchanged. Two comments in the file already recorded that the
cascade depends on it: several selectors in the tome section win only because
they come after the base card rules, and the 720px query overrides the whole
desktop design. The built CSS bundle is byte-identical before and after, at
56686 bytes.

The plan asked for each `@media` block to move next to the rules it overrides.
That is not done, and should not be. Moving a rule past a later rule of equal
specificity changes which one wins, and there is no frontend test that would
catch it. The 720px block is now `responsive.css`, imported last.

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