`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
220 lines
8.3 KiB
CSS
220 lines
8.3 KiB
CSS
/* ============================================================
|
|
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; }
|
|
}
|