: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; } /* ---------- Scrollbars ---------- */ ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 5px; border: 2px solid var(--bg); } ::-webkit-scrollbar-thumb:hover { background: var(--accent-dim); } /* ---------- Top navigation ---------- */ .topnav { display: flex; align-items: center; gap: 26px; padding: 0 28px; height: 56px; background: var(--bg-panel-glass); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); position: sticky; top: 0; z-index: 10; } .topnav .brand { font-family: var(--font-display); font-size: 1.2rem; font-weight: 700; color: var(--accent); letter-spacing: 0.14em; text-shadow: 0 0 18px var(--accent-glow); margin-right: 6px; } .topnav a.navlink { color: var(--text-dim); padding: 17px 2px 15px; font-size: 0.9rem; font-weight: 500; letter-spacing: 0.04em; border-bottom: 2px solid transparent; transition: color 0.15s, border-color 0.2s; } .topnav a.navlink.active { color: var(--accent-bright); border-bottom-color: var(--accent); text-shadow: 0 0 14px var(--accent-glow); } .topnav a.navlink:hover { color: var(--text); } /* On desktop the wrapper is transparent — its children (nav links + account) participate directly in the .topnav flex row, exactly as before. The mobile query below turns it into a dropdown. */ .nav-links { display: contents; } .nav-hamburger { display: none; background: none; border: none; color: var(--text); font-size: 1.2rem; padding: 6px 8px; margin-left: auto; line-height: 1; } /* Phase 8: account area (hosted multi-user mode only) */ .nav-account { margin-left: auto; display: flex; align-items: center; gap: 10px; min-width: 0; } .nav-account button { padding: 5px 12px; font-size: 0.82rem; } .guest-nudge { color: var(--text-dim); font-size: 0.78rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .account-email { color: var(--text-dim); font-size: 0.82rem; max-width: 220px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } button.linklike { background: none; border: none; padding: 0; color: var(--accent); font-size: 0.82rem; cursor: pointer; } button.linklike:hover { text-decoration: underline; } @media (max-width: 900px) { .guest-nudge { display: none; } } /* Phase 8: demo-key banner (Settings) */ .demo-banner { border: 1px solid var(--border-bright); border-left: 3px solid var(--accent); border-radius: 8px; padding: 12px 16px; margin-bottom: 18px; color: var(--text-dim); font-size: 0.88rem; line-height: 1.5; } .demo-banner strong { color: var(--accent-bright); } .page { flex: 1; width: 100%; max-width: 960px; margin: 0 auto; padding: 28px 24px; animation: page-in 0.35s ease both; } @keyframes page-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } } /* ---------- Forms ---------- */ label.field { display: block; margin-bottom: 14px; } label.field .label { display: block; font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.12em; color: var(--text-dim); margin-bottom: 5px; } input[type="text"], input[type="email"], input[type="password"], input[type="number"], textarea, select { width: 100%; background: var(--bg-input); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 9px 12px; font-size: 0.95rem; font-family: inherit; transition: border-color 0.15s, box-shadow 0.15s; } textarea { resize: vertical; min-height: 70px; line-height: 1.5; } input:focus, textarea:focus, select:focus { outline: none; border-color: var(--accent-dim); box-shadow: 0 0 0 3px rgba(212, 169, 78, 0.12); } input::placeholder, textarea::placeholder { color: #5c584d; } button { background: var(--bg-input); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 7px 16px; font-size: 0.88rem; font-weight: 500; cursor: pointer; font-family: inherit; transition: border-color 0.15s, background 0.15s, box-shadow 0.15s, transform 0.1s; } button:hover { border-color: var(--accent-dim); box-shadow: 0 0 12px rgba(212, 169, 78, 0.1); } button:active { transform: translateY(1px); } button.primary { background: linear-gradient(160deg, var(--accent-bright), var(--accent) 55%, #b78d3e); border-color: var(--accent); color: #191307; font-weight: 600; letter-spacing: 0.02em; box-shadow: 0 2px 10px var(--accent-glow); } button.primary:hover { box-shadow: 0 2px 18px rgba(212, 169, 78, 0.45); filter: brightness(1.08); } button.danger { color: var(--danger); } button.danger:hover { border-color: var(--danger); box-shadow: 0 0 12px rgba(208, 101, 101, 0.15); } button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: none; } /* ---------- Cards & lists ---------- */ .card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; } .card { position: relative; background: linear-gradient(170deg, #17172400, #17172400 60%, rgba(212, 169, 78, 0.03)), var(--bg-panel); border: 1px solid var(--border); border-radius: 12px; padding: 18px; cursor: pointer; transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s; overflow: hidden; } .card::before { content: ''; position: absolute; inset: 0 0 auto 0; height: 2px; background: linear-gradient(90deg, transparent, var(--accent-dim), transparent); opacity: 0; transition: opacity 0.25s; } .card:hover { border-color: var(--border-bright); transform: translateY(-3px); box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45), 0 0 20px rgba(212, 169, 78, 0.06); } .card:hover::before { opacity: 1; } .card h3 { margin: 0 0 6px; font-family: var(--font-display); font-size: 1.02rem; color: var(--accent); } .card p { margin: 0; color: var(--text-dim); font-size: 0.88rem; line-height: 1.5; } .card .meta { margin-top: 12px; font-size: 0.75rem; color: var(--text-dim); } .page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } .page-header h1 { margin: 0; font-family: var(--font-display); font-size: 1.55rem; color: var(--accent-bright); text-shadow: 0 0 24px var(--accent-glow); } .empty { color: var(--text-dim); text-align: center; padding: 60px 0; font-style: italic; font-family: var(--font-story); font-size: 1.05rem; } /* ---------- Story card editor ---------- */ .storycard { background: var(--bg-input); border: 1px solid var(--border); border-left: 3px solid var(--accent-dim); border-radius: 8px; padding: 12px; margin-bottom: 10px; transition: border-color 0.15s; } .storycard:focus-within { border-color: var(--accent-dim); } .storycard .row { display: flex; gap: 10px; margin-bottom: 8px; } .storycard .row > * { flex: 1; } /* ---------- Play page ---------- */ .play-page { display: flex; flex-direction: column; min-height: calc(100dvh - 56px); /* Room to scroll the last beat clear of a composer lifted by the keyboard (see --kb-inset in keyboard.js); 0 on desktop and on browsers that shrink the layout viewport themselves. Margin, not padding — .page already sets padding and a longhand here would drop its bottom gutter. */ margin-bottom: var(--kb-inset, 0px); } /* Keep the Plot/Memory/Scripts/Insights toggles reachable while the story scrolls — pin the header just below the 56px top nav. */ .play-page .page-header { position: sticky; top: 56px; z-index: 5; background: var(--bg); margin-bottom: 12px; padding: 12px 0; border-bottom: 1px solid var(--border); } .story { flex: 1; font-family: var(--font-story); font-size: 1.14rem; line-height: 1.7; white-space: pre-wrap; padding-bottom: 16px; } /* The head of a windowed transcript: how much story is still above, and the way back to it. Deliberately quiet — it is a seam in the page, not a feature — and it sits inside .story so it inherits the story face. */ .story-earlier { text-align: center; margin: 4px 0 22px; font-size: 0.9rem; /* No action-in animation here: this appears above content the reader is already looking at, and a fade would read as the story moving. */ } .story-earlier .dim { color: var(--text-dim); font-style: italic; } .story-earlier button { background: none; border: none; border-bottom: 1px solid rgba(159, 199, 209, 0.3); color: var(--text-dim); font: inherit; font-style: italic; cursor: pointer; padding: 2px 4px; } .story-earlier button:hover { color: var(--accent-bright); border-bottom-color: var(--accent-bright); } .story .action { position: relative; margin-bottom: 15px; padding-right: 56px; animation: action-in 0.4s ease both; } @keyframes action-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } .story .action.player { color: var(--player); border-left: 2px solid rgba(159, 199, 209, 0.35); padding-left: 14px; font-style: italic; } .story .action.streaming { color: var(--text); } .story .cursor { color: var(--accent-bright); text-shadow: 0 0 8px var(--accent-glow); animation: blink 1s steps(1) infinite; } @keyframes blink { 50% { opacity: 0; } } /* Reasoning-model thinking, collapsed above the AI's response */ .story .reasoning { margin: 0 0 8px; font-size: 0.82rem; line-height: 1.5; color: var(--text-dim); font-style: normal; } .story .reasoning summary { cursor: pointer; user-select: none; color: var(--text-dim); opacity: 0.8; } .story .reasoning summary:hover { opacity: 1; } .story .reasoning .reasoning-text { margin-top: 6px; padding: 8px 12px; border-left: 2px solid var(--border); opacity: 0.85; max-height: 260px; overflow-y: auto; } .story .action-tools { position: absolute; top: 0; right: 0; display: inline-flex; gap: 4px; opacity: 0; pointer-events: none; transition: opacity 0.15s; } .story .action:hover .action-tools { opacity: 1; pointer-events: auto; } .story .action-tools button { padding: 1px 8px; font-size: 0.72rem; font-family: var(--font-ui); color: var(--text-dim); background: var(--bg-panel-glass); backdrop-filter: blur(6px); } .story .action-tools button:hover { color: var(--text); } /* The 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; } /* ---------- Side panel (Plot / Insights) ---------- */ .play-layout { flex: 1; display: flex; min-height: calc(100dvh - 56px); } .play-layout .play-page { max-width: 960px; } .play-layout.with-panel .play-page { max-width: none; } .panel-toggles { display: flex; gap: 8px; } .panel-toggles button.active { color: var(--accent-bright); border-color: var(--accent-dim); box-shadow: inset 0 0 12px rgba(212, 169, 78, 0.08); } .side-panel { width: 420px; flex-shrink: 0; border-left: 1px solid var(--border); background: var(--bg-panel); padding: 20px; overflow-y: auto; height: calc(100dvh - 56px); position: sticky; top: 56px; animation: panel-in 0.25s ease both; } /* ---------- In-play script viewer ---------- */ .adv-script { border-bottom: 1px solid var(--border); padding: 10px 0; } .adv-script-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; } .adv-script-desc { font-size: 0.82rem; margin-top: 4px; } .linklike { border: none; background: none; padding: 0; color: var(--accent-bright); font-size: 0.8rem; cursor: pointer; flex-shrink: 0; } .linklike:hover { text-decoration: underline; } .adv-script-code { margin-top: 8px; } .adv-script-hook { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-dim); margin: 8px 0 2px; } .adv-script-code pre { margin: 0; padding: 10px; background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; overflow-x: auto; font-family: var(--font-mono, monospace); font-size: 0.76rem; line-height: 1.5; white-space: pre; } /* ---------- Left status drawer (script state) ---------- */ .status-drawer { flex-shrink: 0; position: sticky; top: 56px; height: calc(100dvh - 56px); display: flex; align-items: flex-start; border-right: 1px solid var(--border); background: var(--bg-panel); } .status-drawer.open { width: 300px; } .status-toggle { flex-shrink: 0; height: 100%; width: 30px; padding: 12px 0; border: none; border-radius: 0; background: transparent; color: var(--text-dim); cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 8px; } .status-toggle:hover { color: var(--text); background: var(--bg-input); } .status-toggle-label { writing-mode: vertical-rl; text-orientation: mixed; letter-spacing: 0.12em; font-size: 0.72rem; text-transform: uppercase; } .status-body { flex: 1; min-width: 0; height: 100%; overflow-y: auto; padding: 20px 16px; animation: panel-in 0.2s ease both; } .status-vars { list-style: none; margin: 0; padding: 0; } .status-vars li { display: flex; flex-direction: column; gap: 2px; padding: 8px 0; border-bottom: 1px solid var(--border); } .status-key { font-family: var(--font-mono, monospace); font-size: 0.82rem; color: var(--accent-bright); word-break: break-word; } .status-val { font-family: var(--font-mono, monospace); font-size: 0.82rem; color: var(--text); white-space: pre-wrap; word-break: break-word; } /* Recursive script-state value tree */ .jv-string { color: var(--text); } .jv-number { color: var(--player); } .jv-bool { color: var(--accent-bright); } .jv-null { color: var(--text-dim); font-style: italic; } .jv-empty { color: var(--text-dim); } .jv-tree { display: inline-block; vertical-align: top; max-width: 100%; } .jv-toggle { background: none; border: none; padding: 0; cursor: pointer; font-family: inherit; font-size: 0.72rem; color: var(--text-dim); } .jv-toggle:hover { color: var(--accent-bright); } .jv-children { list-style: none; margin: 2px 0 2px 0; padding-left: 12px; border-left: 1px solid var(--border); } .jv-children li { padding: 2px 0; word-break: break-word; } .jv-key { color: var(--accent); margin-right: 6px; } .jv-key::after { content: ':'; color: var(--text-dim); } @keyframes panel-in { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: none; } } /* ---------- World State drawer (Phase 12 RPG) ---------- */ .ws-group { margin-bottom: 18px; } .ws-group-title { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-dim); margin: 0 0 8px; } .ws-stat { padding: 5px 0; } .ws-stat-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; font-size: 0.85rem; } .ws-stat-name { color: var(--text); text-transform: capitalize; } .ws-stat-val { color: var(--text-dim); font-variant-numeric: tabular-nums; font-size: 0.8rem; text-align: right; } .ws-band { color: var(--accent-bright); } .ws-bar { margin-top: 4px; height: 5px; border-radius: 3px; background: var(--bg-input); overflow: hidden; } .ws-bar-fill { height: 100%; border-radius: 3px; background: var(--accent); transition: width 0.35s ease; } /* Cast — each NPC is a small plate inside the one "Cast" group, so a roster of five doesn't read as five more unrelated sections in the rail. */ .ws-npc { padding: 7px 9px; margin-bottom: 7px; border: 1px solid var(--border); border-left: 2px solid var(--accent-dim); border-radius: 7px; background: rgba(255, 255, 255, 0.022); } .ws-npc:last-child { margin-bottom: 0; } .ws-npc-head { display: flex; align-items: center; gap: 7px; margin-bottom: 3px; } .ws-npc-avatar { flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center; font-family: var(--font-display); font-size: 0.6rem; color: #14100a; background: linear-gradient(150deg, var(--accent-bright), var(--accent-dim)); } .ws-npc-name { font-family: var(--font-display); font-size: 0.85rem; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ws-none { font-size: 0.78rem; color: var(--text-dim); font-style: italic; } .ws-milestones { list-style: none; margin: 0; padding: 0; } .ws-milestones li { display: flex; gap: 7px; align-items: flex-start; padding: 4px 0; font-size: 0.85rem; color: var(--text); } .ws-milestones li.done { color: var(--text-dim); } .ws-check { flex-shrink: 0; color: var(--accent-bright); } .ws-milestones li.done .ws-check { color: var(--player); } .ws-flags { list-style: none; margin: 0; padding: 0; } .ws-flags li { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 0.85rem; color: var(--text); } .ws-flag-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; background: var(--bg-input); border: 1px solid var(--border); } .ws-flag-dot.on { background: var(--accent); border-color: var(--accent); } .ws-flag-name { flex: 1; } .ws-flag-val { color: var(--text-dim); font-size: 0.8rem; } .ws-flag-edit { display: flex; align-items: center; gap: 8px; cursor: pointer; width: 100%; } .ws-edit-actions { display: flex; gap: 6px; } .ws-edit-input { width: 100%; margin-top: 3px; padding: 4px 7px; font-size: 0.82rem; border-radius: 6px; } .schema-editor { width: 100%; font-family: var(--font-mono, monospace); font-size: 0.82rem; line-height: 1.5; tab-size: 2; white-space: pre; overflow-x: auto; } .schema-error { margin-top: 6px; color: var(--danger, #e5484d); font-size: 0.82rem; } .schema-json-actions { display: flex; align-items: center; gap: 12px; margin-top: 8px; } .schema-json-actions .schema-error { margin-top: 0; } .schema-preview { margin-top: 14px; padding: 14px 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-panel); } .schema-preview-title { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--text-dim); margin-bottom: 10px; } .schema-group { margin-bottom: 16px; } .schema-group:last-child { margin-bottom: 0; } .schema-group-title { font-family: Georgia, serif; font-size: 0.98rem; color: var(--accent-bright); margin-bottom: 6px; } .schema-npc-id { font-family: var(--font-mono, monospace); font-size: 0.72rem; color: var(--text-dim); margin-left: 6px; } .schema-stat { padding: 4px 0; border-bottom: 1px solid var(--border); } .schema-stat:last-child { border-bottom: none; } .schema-stat-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; } .schema-stat-name { color: var(--text); text-transform: capitalize; } .schema-stat-meta { color: var(--text-dim); font-size: 0.8rem; font-variant-numeric: tabular-nums; text-align: right; } .schema-desc { color: var(--text-dim); font-size: 0.8rem; margin-top: 2px; } .schema-bands { color: var(--text-dim); font-size: 0.76rem; margin-top: 2px; font-style: italic; } /* ---- Form-based schema editor ---- One shape for everything: a section is a panel, every entry inside it is a `.se-row` tile, and every control wears a `.se-field` caption. Adding a new kind of entry means reusing those three, not inventing a fourth box. */ .se-section { margin-bottom: 16px; padding: 13px 15px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg-panel); } .se-section-nested { margin: 10px 0 0; padding: 11px 12px; background: rgba(0, 0, 0, 0.22); border-radius: 8px; } .se-section-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; } .se-section-title { font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent); } .se-hint { margin: -2px 0 10px; font-size: 0.78rem; line-height: 1.45; color: var(--text-dim); } .se-empty { color: var(--text-dim); font-size: 0.82rem; padding: 4px 0; } .se-add, .se-add-sm { font-size: 0.78rem; padding: 3px 10px; white-space: nowrap; } .se-add-sm { margin-top: 6px; } .se-remove { background: transparent; border: none; color: var(--text-dim); cursor: pointer; padding: 2px 6px; font-size: 0.9rem; line-height: 1; flex-shrink: 0; } .se-remove:hover { color: var(--danger, #e5484d); } /* Every control in the editor, one look. The global field rule keys off `input[type="…"]`, so the plain ``s here (keys, descriptions) never matched it and fell back to browser defaults — that mismatch is most of why this panel used to read as a pile of unrelated boxes. Everything below is qualified with `.se` so it outranks that global rule (0,1,1) too. */ /* `:where()` so the exclusion costs no specificity — a plain `:not([type="checkbox"])` would rank this above the `.se .se-*` overrides below and swallow every one of them. */ .se input:not(:where([type="checkbox"])), .se textarea, .se select { width: 100%; background: var(--bg-input); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 5px 8px; font-size: 0.84rem; font-family: inherit; transition: border-color 0.15s, box-shadow 0.15s; } /* One entry — a stat, a flag, a milestone. */ .se-row { padding: 10px 12px; margin-bottom: 8px; border: 1px solid var(--border); border-radius: 8px; background: rgba(255, 255, 255, 0.018); transition: border-color 0.15s; } .se-row:last-child { margin-bottom: 0; } .se-row:focus-within { border-color: var(--border-bright); } /* Wraps because a phone-width row can't fit key + toggle + ✕ side by side, and an input's default `min-width: auto` would push the ✕ off-screen instead. */ .se-row-top { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 8px; } .se .se-key { font-family: var(--font-mono, monospace); font-size: 0.82rem; min-width: 0; max-width: 220px; } /* Only in the row context — `.se-field` is a *column* flex, where a basis of 140px would set the input's height instead of its width. */ .se .se-row-top .se-key { flex: 1 1 140px; } .se-field { display: flex; flex-direction: column; gap: 3px; } .se-field > span { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.09em; color: var(--text-dim); } .se-fields { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: flex-end; margin-bottom: 8px; } .se .se-num input, .se .se-num select { width: 72px; padding: 4px 6px; } .se .se-kind select { width: 132px; } .se .se-initial-text input { width: 220px; max-width: 100%; } .se-check { display: flex; align-items: center; gap: 5px; font-size: 0.8rem; color: var(--text-dim); white-space: nowrap; } .se-check input { width: auto; } /* The global textarea rule also forces min-height:70px, which a 2-row inline field doesn't want. */ .se .se-area { min-height: 0; line-height: 1.45; resize: vertical; } .se-sub-head { font-size: 0.7rem; color: var(--text-dim); margin: 4px 0 4px; } .se-band { display: flex; gap: 6px; align-items: center; margin-bottom: 4px; } /* Qualified so it outweighs the global input[type="number"] rule (0,1,1) — as a bare class it lost, and the band bounds rendered full-width. */ .se .se-band-n { width: 64px; padding: 4px 6px; } .se .se-band-l { flex: 1 1 60px; width: auto; min-width: 0; } .se-bands { margin-top: 8px; } /* ---- NPC roster (its own page section) ---- */ .se-npc-grid { display: grid; /* min() so a 320px phone gets one column instead of a 340px overflow */ grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr)); gap: 14px; align-items: start; } .se-npc { border: 1px solid var(--border); border-radius: 10px; background: var(--bg-panel); overflow: hidden; } .se-npc:focus-within { border-color: var(--border-bright); } .se-npc-head { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-bottom: 1px solid var(--border); background: linear-gradient(180deg, rgba(212, 169, 78, 0.07), transparent); } .se-avatar { flex-shrink: 0; width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; font-family: var(--font-display); font-size: 0.8rem; letter-spacing: 0.02em; color: #14100a; background: linear-gradient(150deg, var(--accent-bright), var(--accent-dim)); } .se-npc-ident { flex: 1; min-width: 0; } .se .se-npc-name { padding: 2px 6px; font-family: var(--font-display); font-size: 0.95rem; background: transparent; border-color: transparent; } .se .se-npc-name:focus { background: var(--bg-input); border-color: var(--border-bright); } .se-npc-addr { display: block; margin: 1px 0 0 6px; font-family: var(--font-mono, monospace); font-size: 0.7rem; color: var(--text-dim); } .se-npc-count { flex-shrink: 0; font-size: 0.7rem; color: var(--text-dim); white-space: nowrap; } .se-npc-body { padding: 12px; display: flex; flex-direction: column; gap: 10px; } .se-npc-idfield .se-key { max-width: 100%; } .ws-report { list-style: none; margin: 4px 0 0; padding: 0; font-size: 0.82rem; } .ws-report li { padding: 2px 0; color: var(--text); } /* Inline per-turn change chips under an AI message */ .turn-changes { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } .chg { font-size: 0.74rem; padding: 2px 8px; border-radius: 10px; border: 1px solid var(--border); background: var(--bg-input); color: var(--text-dim); font-variant-numeric: tabular-nums; /* A chip must never be wider than the column it sits in: an NPC stat like "bandit leader aggression +10" overruns a phone, and the chip row is inside the story text, which has nothing to scroll. Text may break mid-word as a last resort, which is what keeps a chip's minimum width small enough for the flex row to shrink it. */ max-width: 100%; overflow-wrap: anywhere; } .chg-val { overflow-wrap: anywhere; } /* Only a signed number is held on one line, so the sign never ends up on a line of its own. The other values a chip can carry are phrases — a refusal reason, "no change — at its limit", or the whole new value of a free-text stat — and holding those together set the chip's minimum width to the width of the phrase, which is what pushed a chip out of the message on a phone. */ .chg-num { white-space: nowrap; } .chg-up { color: var(--player, #4caf82); border-color: color-mix(in srgb, var(--player, #4caf82) 40%, var(--border)); } .chg-down { color: var(--danger, #e5484d); border-color: color-mix(in srgb, var(--danger, #e5484d) 40%, var(--border)); } .chg-flag { color: var(--accent-bright); } .chg-ms { color: var(--accent-bright); border-color: color-mix(in srgb, var(--accent-bright) 40%, var(--border)); } /* A refused change, and a clamp that left the value where it started. The dashed border separates it from the changes that took effect. It is not styled as an error, because the rules refusing a change is them working. */ .chg-refused { border-style: dashed; color: var(--text-dim); border-color: color-mix(in srgb, var(--danger) 30%, var(--border)); } /* A clamp that reduced a change but still moved the value. */ .chg-limited { opacity: 0.75; font-style: italic; } .ws-report code { color: var(--accent-bright); } .ws-delta { color: var(--player); font-variant-numeric: tabular-nums; } .ws-flag { color: var(--text-dim); font-style: italic; } .ws-rejected { color: var(--text-dim); } .side-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } .side-panel-header h2 { margin: 0; font-family: var(--font-display); font-size: 1.05rem; color: var(--accent); } /* ---------- Insights ---------- */ .insights-meta { margin-bottom: 16px; font-size: 0.85rem; } .insights-mode { color: var(--text-dim); margin-bottom: 8px; } .insights-mode button { padding: 2px 8px; font-size: 0.75rem; } .token-total { font-size: 1rem; color: var(--text); margin-bottom: 4px; } .token-total.over { color: var(--danger); } /* Stacked share-of-context bar. Segment colours come from SECTION_COLORS in Play.jsx so the bar, its legend and each section header stay in sync. */ .token-split { display: flex; height: 14px; background: var(--bg-input); border-radius: 4px; overflow: hidden; margin-bottom: 8px; } .token-split.over { box-shadow: 0 0 0 1px var(--danger); } .token-seg { min-width: 2px; /* a 0.2% section is still worth seeing */ border-right: 1px solid var(--bg-panel); transition: opacity 0.15s ease; } .token-seg:last-child { border-right: none; } .token-seg-free { background: transparent; min-width: 0; border-right: none; } .token-seg.faded, .token-legend-item.faded { opacity: 0.25; } .token-legend { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 1px 12px; margin-bottom: 10px; } .token-legend-item { display: flex; align-items: center; gap: 7px; background: none; border: none; border-radius: 4px; padding: 2px 5px; font-size: 0.78rem; color: var(--text-dim); text-align: left; transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease; } .token-legend-item:hover { background: rgba(255, 255, 255, 0.05); color: var(--text); } .token-swatch { width: 9px; height: 9px; border-radius: 2px; flex: none; } .token-legend-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .token-legend-pct { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; } .token-legend-tok { width: 4.5em; text-align: right; opacity: 0.65; font-variant-numeric: tabular-nums; } .token-legend-more { justify-self: start; background: none; border: none; padding: 2px 5px; font-size: 0.75rem; color: var(--text-dim); } .token-legend-more:hover { color: var(--accent); background: none; } .insights-history { color: var(--text-dim); margin-bottom: 8px; } .insights-cards div { margin-bottom: 3px; } .insights-cards .dropped { color: var(--danger); } .ctx-section { margin-bottom: 12px; border-left: 3px solid var(--border); background: rgba(255, 255, 255, 0.015); border-radius: 0 6px 6px 0; } .ctx-section .ctx-header { display: flex; justify-content: space-between; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-dim); padding: 5px 10px 3px; } .ctx-section pre { margin: 0; padding: 6px 10px 8px; white-space: pre-wrap; word-break: break-word; font-family: Consolas, monospace; font-size: 0.8rem; line-height: 1.5; color: var(--text); max-height: 260px; overflow-y: auto; } /* Per-section border/header colours are applied inline from SECTION_COLORS (Play.jsx) — one palette drives the bar, the legend and these headers. */ /* ---------- Scripts ---------- */ .slot-tabs { display: flex; gap: 0; margin-bottom: 4px; } .slot-tabs button { border-radius: 0; border-right-width: 0; color: var(--text-dim); } .slot-tabs button:first-child { border-radius: 8px 0 0 8px; } .slot-tabs button:last-child { border-radius: 0 8px 8px 0; border-right-width: 1px; } .slot-tabs button.active { color: var(--accent-bright); background: var(--bg-panel); } .slot-hint { color: var(--text-dim); font-size: 0.8rem; margin-bottom: 8px; } .cm-editor { border: 1px solid var(--border); border-radius: 8px; font-size: 0.88rem; } .cm-editor.cm-focused { outline: none; border-color: var(--accent-dim); box-shadow: 0 0 0 3px rgba(212, 169, 78, 0.1); } .test-run .row { display: flex; gap: 12px; } .test-result { margin-top: 12px; background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; font-size: 0.85rem; } .test-result .label { text-transform: uppercase; font-size: 0.7rem; letter-spacing: 0.08em; color: var(--text-dim); margin-right: 6px; } .test-result pre { display: inline-block; margin: 2px 0; white-space: pre-wrap; word-break: break-word; font-family: Consolas, monospace; vertical-align: top; } .test-error { color: var(--danger); white-space: pre-wrap; } .script-attach { display: flex; align-items: baseline; gap: 8px; padding: 9px 12px; background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 6px; cursor: pointer; transition: border-color 0.15s; } .script-attach:hover { border-color: var(--border-bright); } .script-attach .dim { color: var(--text-dim); font-size: 0.82rem; } /* ---------- Memory bank (Phase 6) ---------- */ .memory-row { background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px; padding: 9px 12px; margin-bottom: 6px; } .memory-row.forgotten { opacity: 0.55; } .memory-text { font-size: 0.9rem; white-space: pre-wrap; } .memory-meta { display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px; margin-top: 5px; font-size: 0.78rem; } .memory-meta .dim { color: var(--text-dim); } .memory-meta .action-tools { display: inline-flex; gap: 4px; margin-left: auto; } .memory-meta .action-tools button { padding: 1px 8px; font-size: 0.72rem; color: var(--text-dim); } .memory-meta .action-tools button:hover { color: var(--text); } .memory-badge { color: var(--accent); border: 1px solid var(--border); border-radius: 999px; padding: 0 8px; } .script-report { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 8px; } .script-error { color: var(--danger); font-size: 0.85rem; margin-bottom: 4px; white-space: pre-wrap; } .script-logs { background: var(--bg-input); border-radius: 8px; padding: 8px 10px; font-family: Consolas, monospace; font-size: 0.8rem; white-space: pre-wrap; word-break: break-word; margin: 6px 0; } /* ---------- Filter bar & tags ---------- */ .filter-bar { margin-bottom: 18px; } .search-input { max-width: 340px; } .tag-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; } .tag { padding: 2px 12px; font-size: 0.76rem; border-radius: 999px; color: var(--text-dim); } .tag.active { color: var(--accent-bright); border-color: var(--accent-dim); box-shadow: inset 0 0 10px rgba(212, 169, 78, 0.1); } .tag.small { display: inline-block; background: var(--bg-input); border: 1px solid var(--border); padding: 1px 9px; margin-right: 4px; cursor: default; } /* ---------- Modal ---------- */ .modal-overlay { position: fixed; inset: 0; background: rgba(5, 5, 9, 0.7); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 50; animation: page-in 0.2s ease both; } .modal { width: min(480px, calc(100vw - 40px)); max-height: 80vh; overflow-y: auto; background: var(--bg-panel); border: 1px solid var(--border-bright); border-radius: 14px; padding: 26px 28px; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6), 0 0 40px rgba(212, 169, 78, 0.06); } .modal h2 { margin: 0 0 4px; font-family: var(--font-display); font-size: 1.2rem; color: var(--accent-bright); } .modal .modal-hint { margin: 0 0 18px; color: var(--text-dim); font-size: 0.88rem; } .modal .modal-buttons { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; } /* ---------- Auth (log in / sign up) ---------- */ .auth-modal { width: min(400px, calc(100vw - 40px)); text-align: center; padding-top: 22px; } .auth-crest { font-size: 1.1rem; color: var(--accent-dim); letter-spacing: 0.3em; margin-bottom: 6px; } .auth-modal h2 { font-size: 1.35rem; } .auth-modal .auth-hint { margin-bottom: 16px; line-height: 1.45; } /* Fields go back to left-aligned — only the header is centred. */ .auth-modal label.field { text-align: left; } .auth-tabs { display: flex; gap: 4px; padding: 4px; margin-bottom: 18px; background: var(--bg-input); border: 1px solid var(--border); border-radius: 10px; } .auth-tab { flex: 1; padding: 7px 0; border: 1px solid transparent; border-radius: 7px; background: transparent; color: var(--text-dim); font-family: var(--font-display); font-size: 0.78rem; letter-spacing: 0.1em; text-transform: uppercase; } .auth-tab:hover { color: var(--text); background: rgba(212, 169, 78, 0.06); } .auth-tab.active { background: var(--bg-panel); border-color: var(--accent-dim); color: var(--accent-bright); } .auth-password { display: flex; gap: 8px; } .auth-password input { flex: 1; min-width: 0; } .auth-reveal { flex: none; padding: 0 12px; font-size: 0.78rem; color: var(--text-dim); } .auth-reveal:hover { color: var(--accent-bright); } .auth-help { display: block; margin-top: 6px; font-size: 0.76rem; color: var(--text-dim); } .auth-error { margin-top: 4px; padding: 9px 12px; border: 1px solid rgba(208, 101, 101, 0.45); border-radius: 8px; background: rgba(208, 101, 101, 0.09); color: var(--danger); font-size: 0.85rem; text-align: left; white-space: pre-wrap; } /* "Update from scenario" — a wider modal, since it lists a diff. */ .refresh-modal { width: min(620px, calc(100vw - 40px)); } .refresh-changes { border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; font-size: 0.86rem; } .refresh-change + .refresh-change { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); } .refresh-old, .refresh-new { margin-top: 3px; font-family: var(--font-mono, monospace); font-size: 0.8rem; } .refresh-old { text-decoration: line-through; } .refresh-new { color: var(--accent-bright); } /* The button sits above the plot fields it would overwrite. */ .plot-source { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--border); font-size: 0.82rem; } .plot-source .linklike { flex-shrink: 0; } /* ---------- Debug log (Settings) ---------- */ .debug-entry { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; overflow: hidden; } .debug-entry-head { display: flex; align-items: baseline; gap: 12px; padding: 9px 12px; cursor: pointer; font-size: 0.86rem; background: var(--bg-input); } .debug-entry-head .dim, .debug-entry-body .dim { color: var(--text-dim); font-size: 0.8rem; } .debug-status { font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; padding: 1px 8px; border-radius: 999px; border: 1px solid var(--border-bright); color: var(--text-dim); } .debug-status.ok { color: #7dc9a2; border-color: #7dc9a244; } .debug-status.error { color: var(--danger); border-color: var(--danger); } .debug-status.pending { color: var(--accent); border-color: var(--accent-dim); } .debug-entry-body { padding: 10px 12px; font-size: 0.85rem; } .debug-entry-body pre { margin: 2px 0 8px; padding: 8px 10px; background: var(--bg-input); border-radius: 6px; white-space: pre-wrap; word-break: break-word; font-family: Consolas, monospace; font-size: 0.78rem; max-height: 300px; overflow-y: auto; } /* ---------- Play screen error/success banner ---------- Persistent (no auto-dismiss) and can carry a Retry action, so it stays separate from the transient global toasts further down this file. */ .play-toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: var(--bg-panel-glass); backdrop-filter: blur(10px); border: 1px solid var(--danger); color: var(--text); padding: 11px 20px; border-radius: 10px; z-index: 100; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5); animation: toast-in 0.25s ease both; } .play-toast.ok { border-color: var(--accent); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5), 0 0 20px var(--accent-glow); } @keyframes toast-in { from { opacity: 0; transform: translate(-50%, 12px); } to { opacity: 1; transform: translate(-50%, 0); } } /* ============================================================ Illuminated tome — ambient layer, ornament, art plates, cards Everything below styles the home/library surfaces. It comes after the base .card rules on purpose: several selectors here tie with earlier ones on specificity and rely on source order to win. ============================================================ */ /* ---------- Ambient drifting motes ---------- */ /* Behind every page: fixed, unclickable, and out of the stacking flow. The canvas is sized in JS (see Embers.jsx); z-index 0 with the app's own surfaces at 1+ keeps it under content without needing a negative index (which would drop it behind the body background). */ .embers { position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: 0.85; } /* .topnav already carries z-index 10 — deliberately not touched here, so it keeps sitting above page content as well as above the canvas. */ .page { position: relative; z-index: 1; } /* A third gradient pool warming the middle of the ground. `background` is a shorthand, so background-attachment has to be restated or it resets to scroll and the gradients start sliding with the page. */ body { background: radial-gradient(1200px 700px at 15% -10%, rgba(212, 169, 78, 0.07), transparent 60%), radial-gradient(1000px 600px at 90% 110%, rgba(88, 76, 140, 0.1), transparent 55%), radial-gradient(760px 520px at 50% 42%, rgba(150, 119, 58, 0.05), transparent 62%), var(--bg); background-attachment: fixed; } /* ---------- Home banner ---------- */ .hall { display: flex; flex-direction: column; gap: 5px; padding: 6px 0 26px; margin-bottom: 4px; border-bottom: 1px solid var(--border); position: relative; } /* Gold hairline that fades out toward both ends, sat on the border above. */ .hall::after { content: ''; position: absolute; bottom: -1px; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, var(--accent-dim) 22%, var(--accent-dim) 78%, transparent); opacity: 0.55; } .hall-eyebrow { margin: 0; font-size: 0.68rem; letter-spacing: 0.24em; text-transform: uppercase; color: var(--accent-dim); font-weight: 600; } .hall-title { margin: 0; font-family: var(--font-display); font-size: clamp(1.7rem, 4.6vw, 2.3rem); font-weight: 600; letter-spacing: 0.05em; color: var(--accent-bright); text-shadow: 0 0 30px var(--accent-glow); text-wrap: balance; } .hall-sub { margin: 0; color: var(--text-dim); font-family: var(--font-story); font-size: 1.02rem; } /* ---------- Ornamental section rules ---------- */ .home-section { margin-top: 34px; } .rule-head { display: flex; align-items: center; gap: 16px; margin-bottom: 16px; } .rule { flex: 1; display: flex; align-items: center; gap: 14px; margin: 0; font-family: var(--font-display); font-size: 0.82rem; font-weight: 600; letter-spacing: 0.26em; text-transform: uppercase; color: var(--accent-bright); white-space: nowrap; } /* Rules grow to fill whatever the label leaves — the label stays optically centred in its own space without needing a fixed width. */ .rule::before, .rule::after { content: ''; height: 1px; flex: 1; background: linear-gradient(90deg, transparent, var(--accent-dim) 45%, var(--accent-dim) 55%, transparent); } .rule::before { flex: 0 0 26px; } .rule > span { flex: none; } button.linklike.ornate { font-family: var(--font-display); font-size: 0.74rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent-dim); white-space: nowrap; transition: color 0.15s; } button.linklike.ornate:hover { color: var(--accent-bright); text-decoration: none; } /* ---------- Art plates ---------- */ .art { width: 38px; height: 38px; flex: none; border-radius: 3px; border: 1px solid var(--accent-dim); box-shadow: inset 0 0 0 1px rgba(10, 10, 15, 0.7); overflow: hidden; display: grid; place-items: center; line-height: 1; } .art-lg { width: 46px; height: 46px; } .art-photo img { width: 100%; height: 100%; object-fit: cover; display: block; } .art-icon { font-size: 1.15rem; } .art-lg.art-icon { font-size: 1.4rem; } .art-mono { font-family: var(--font-display); font-size: 0.82rem; font-weight: 700; letter-spacing: 0.06em; color: rgba(232, 196, 118, 0.92); text-shadow: 0 1px 4px rgba(0, 0, 0, 0.65); } .art-lg.art-mono { font-size: 0.95rem; } /* ---------- Tome cards ---------- */ .card-grid.wide { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); } .card.tome { border-radius: 3px; padding: 16px 17px 14px; background: linear-gradient(168deg, rgba(212, 169, 78, 0.05), transparent 55%), var(--bg-panel); /* Inner dark gutter + a gold keyline: the double-rule border of a plate in an old book, done with shadows so it costs no extra element. */ box-shadow: inset 0 0 0 1px rgba(10, 10, 15, 0.9), inset 0 0 0 2px rgba(150, 119, 58, 0.22); } /* Filigree corner brackets. These override the base .card::before gradient bar (equal specificity, later in the file wins). */ .card.tome::before, .card.tome::after { content: ''; position: absolute; width: 12px; height: 12px; border: 1px solid var(--accent-dim); opacity: 0.5; transition: opacity 0.2s; background: none; inset: auto; } .card.tome::before { top: 6px; left: 6px; border-right: 0; border-bottom: 0; } .card.tome::after { bottom: 6px; right: 6px; border-left: 0; border-top: 0; } .card.tome:hover::before, .card.tome:hover::after { opacity: 1; } .card.tome:hover { border-color: var(--accent-dim); box-shadow: inset 0 0 0 1px rgba(10, 10, 15, 0.9), inset 0 0 0 2px rgba(212, 169, 78, 0.4), 0 12px 30px rgba(0, 0, 0, 0.5), 0 0 26px rgba(212, 169, 78, 0.08); } .card-head { display: flex; gap: 12px; align-items: center; margin-bottom: 10px; } /* Keep headings clear of the wax seal, which is absolutely positioned over the card's top-right corner. Only cards that actually carry one pay the indent, so scenario cards keep their full width. */ .card.tome:has(> .seal) .card-head { padding-right: 26px; } .card-headings { min-width: 0; } .card.tome h3 { margin: 0; font-family: var(--font-display); font-size: 1rem; font-weight: 600; letter-spacing: 0.045em; color: var(--accent); overflow-wrap: anywhere; } .card-from { margin: 3px 0 0; font-size: 0.75rem; color: var(--text-dim); overflow-wrap: anywhere; } /* Wax seal marking a story in progress. */ .seal { position: absolute; top: 12px; right: 12px; width: 24px; height: 24px; border-radius: 50%; background: radial-gradient(circle at 34% 30%, #b8553f, #7d2f24 72%); box-shadow: 0 1px 6px rgba(0, 0, 0, 0.6), inset 0 -1px 3px rgba(0, 0, 0, 0.45); display: grid; place-items: center; font-size: 0.6rem; color: #f0d5b0; z-index: 1; } .snippet { margin: 0; font-family: var(--font-story); font-size: 0.98rem; line-height: 1.5; color: #b3ac9c; /* Clamp so a long beat can't stretch one card taller than its row. */ display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } .snippet.muted { color: var(--text-dim); font-style: italic; } .snippet.dropcap::first-letter { float: left; font-family: var(--font-display); font-size: 2.3rem; line-height: 0.85; font-weight: 700; color: var(--accent); padding: 3px 7px 0 0; } .card-foot { display: flex; align-items: center; gap: 8px; margin-top: 13px; padding-top: 11px; border-top: 1px solid var(--border); font-size: 0.74rem; color: var(--text-dim); flex-wrap: wrap; } .card-foot .turns { font-variant-numeric: tabular-nums; } .card-foot .dot { opacity: 0.5; } .card-foot .resume { margin-left: auto; color: var(--accent-dim); font-family: var(--font-ui); letter-spacing: 0.1em; text-transform: uppercase; font-size: 0.68rem; font-weight: 600; transition: color 0.15s; } .card.tome:hover .resume { color: var(--accent-bright); } .tag-cluster { display: flex; gap: 5px; flex-wrap: wrap; min-width: 0; } .card-actions { margin-left: auto; display: inline-flex; gap: 5px; } button.tiny { padding: 2px 9px; font-size: 0.72rem; } button.primary.compact { padding: 3px 12px; font-size: 0.76rem; margin-left: auto; } /* Staggered card entrance. The per-card delay is set inline. */ .enter { animation: card-in 0.4s ease both; } @keyframes card-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } } /* ---------- Loading skeletons ---------- */ .skeleton-card { cursor: default; pointer-events: none; animation: card-in 0.3s ease both; } .skeleton-card::before, .skeleton-card::after { content: none; } .skeleton-card:hover { transform: none; border-color: var(--border); box-shadow: none; } .sk { border-radius: 3px; background: linear-gradient(90deg, #1c1c2b 25%, #262639 50%, #1c1c2b 75%); background-size: 220% 100%; animation: shimmer 1.4s ease-in-out infinite; } .sk-plate { width: 38px; height: 38px; margin-bottom: 12px; } .sk-title { height: 13px; width: 58%; margin-bottom: 12px; } .sk-line { height: 9px; margin-bottom: 8px; } @keyframes shimmer { from { background-position: 120% 0; } to { background-position: -120% 0; } } /* ---------- Global toasts ---------- */ .toast-host { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 120; display: flex; flex-direction: column-reverse; gap: 8px; align-items: center; width: max-content; max-width: min(92vw, 520px); pointer-events: none; } .toast { pointer-events: auto; display: flex; align-items: flex-start; gap: 9px; text-align: left; font-family: var(--font-ui); font-size: 0.86rem; line-height: 1.45; padding: 10px 16px; border-radius: 8px; background: var(--bg-panel-glass); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid var(--border-bright); border-left: 3px solid var(--accent); color: var(--text); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5); animation: rise 0.25s ease both; } .toast:hover { border-color: var(--accent-dim); } .toast-error { border-left-color: var(--danger); } .toast-mark { color: var(--accent); flex: none; line-height: 1.5; } .toast-error .toast-mark { color: var(--danger); } @keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } } /* ---------- Story text polish ---------- */ /* Drop cap opening the story's first narrated beat. */ .story .action.opening::first-letter { float: left; font-family: var(--font-display); font-size: 3.1rem; line-height: 0.82; font-weight: 700; color: var(--accent); padding: 5px 10px 0 2px; text-shadow: 0 0 22px var(--accent-glow); } /* Ornamental separator between turns — a diamond flanked by fading rules. Marks the beat change without the heaviness of a full