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:
co-authored by
Claude Opus 5
parent
8422ff24f6
commit
3a9090c20a
+28
-2865
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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); }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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); }
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user