/* The story screen: layout, transcript, composer. * * `BROWSER-UX-SPEC.md` §82-84 and §98. Three rules do most of the work here: * * §82 prose gets a readable measure and does not stretch across a wide * monitor, whatever the window is doing; * §83 no card chrome around every message — the story reads as prose, and a * player turn is set apart by indentation and colour rather than a box; * §84 no metadata on screen, so the controls on a turn stay invisible until * the pointer or the keyboard reaches that turn. * * The transcript scrolls the page rather than an inner element. That keeps the * browser's own find-in-page, scroll restoration and keyboard paging working, * which an inner scroller quietly breaks. */ /* ---------- Layout ---------- */ .play { display: flex; flex-direction: column; min-height: 100vh; } .play-header { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 20px; /* Opaque, not glass. The first browser pass had story text reading straight through a translucent bar; a header the story shows through is not a header. */ background: var(--bg-panel); border-bottom: 1px solid var(--border); } .play-title { display: flex; align-items: baseline; gap: 14px; min-width: 0; flex: 1; } .play-title h1 { margin: 0; font-size: 1.02rem; font-weight: 600; letter-spacing: 0.02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .play-title .back { font-size: 0.78rem; color: var(--text-dim); white-space: nowrap; } .play-title .back:hover { color: var(--accent); } .play-header-right { display: flex; align-items: center; gap: 16px; flex-shrink: 0; } .panel-tabs { display: flex; gap: 2px; } .panel-tabs button { padding: 5px 11px; font-size: 0.76rem; color: var(--text-dim); background: transparent; border: 1px solid transparent; border-radius: 5px; } .panel-tabs button:hover { color: var(--text); border-color: var(--border); } .panel-tabs button.active { color: var(--accent-bright); border-color: var(--border-bright); background: var(--bg-input); } .play-main { display: flex; flex: 1; align-items: flex-start; min-height: 0; } .play-story { flex: 1; min-width: 0; display: flex; flex-direction: column; /* Fills the viewport below the header, so the composer sits at the bottom of a short story rather than immediately under its last line. */ min-height: calc(100vh - 52px); } /* ---------- Transcript ---------- */ .story { /* §82. The measure is the point: prose is read, not scanned, and 66ch is about the width a line can be before the eye loses the next one. */ width: min(66ch, 100%); margin: 0 auto; /* The bottom padding clears the sticky composer. Enough to scroll the last line clear of it, not so much that a short story opens on a void. */ padding: 36px 24px 40px; flex: 1; font-family: var(--font-story); font-size: 1.14rem; line-height: 1.72; } .story-empty { color: var(--text-dim); font-style: italic; } .turn { position: relative; margin: 0 0 1.35em; animation: turn-in 0.35s ease both; } @keyframes turn-in { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; } } .turn-text > *:first-child { margin-top: 0; } .turn-text > *:last-child { margin-bottom: 0; } .turn-text p { margin: 0 0 0.75em; } /* §83. A player turn is the reader's own voice: indented, lighter, in the cooler of the two accents. No box, no avatar, no timestamp. */ .turn-player .turn-text { padding-left: 1.1em; border-left: 2px solid var(--player); color: var(--player); font-style: italic; } .turn-narrator .turn-text { color: var(--text); } /* The opening beat gets the drop cap, and only that one. */ .turn-opening .turn-text > p:first-of-type::first-letter { float: left; font-family: var(--font-display); font-size: 3.1em; line-height: 0.82; padding: 0.06em 0.09em 0 0; color: var(--accent); } .scene-break { margin: 1.8em 0; text-align: center; color: var(--border-bright); font-size: 0.8rem; letter-spacing: 0.5em; } .story .cursor { display: inline-block; margin-left: 1px; color: var(--accent); animation: blink 1s steps(2) infinite; } @keyframes blink { 50% { opacity: 0; } } .story .reasoning { margin-bottom: 0.5em; font-family: var(--font-ui); font-size: 0.78rem; } .story .reasoning summary { cursor: pointer; color: var(--text-dim); opacity: 0.7; } .story .reasoning summary:hover { opacity: 1; } .story .reasoning .reasoning-text { margin-top: 5px; padding-left: 10px; border-left: 1px solid var(--border); color: var(--text-dim); white-space: pre-wrap; } /* §84. Controls stay out of the way until the turn is reached — but they are real buttons in the tab order, so `:focus-within` has to reveal them or a keyboard reader would be operating something they cannot see. */ .turn-tools { display: flex; gap: 4px; margin-top: 6px; font-family: var(--font-ui); opacity: 0; transition: opacity 0.15s; } .turn:hover .turn-tools, .turn-tools:focus-within { opacity: 1; } .turn-tools button { padding: 2px 8px; font-size: 0.71rem; color: var(--text-dim); background: transparent; border: 1px solid transparent; border-radius: 4px; } .turn-tools button:hover { color: var(--text); border-color: var(--border-bright); } .turn-edit { margin: 0 0 1.35em; } .turn-edit textarea { width: 100%; font-family: var(--font-story); font-size: 1.1rem; line-height: 1.6; } .edit-buttons { display: flex; gap: 8px; margin-top: 8px; } .edit-hint { margin: 8px 0 0; font-family: var(--font-ui); font-size: 0.76rem; color: var(--text-dim); } .story-earlier { margin-bottom: 1.6em; text-align: center; font-family: var(--font-ui); } .story-earlier button { padding: 4px 14px; font-size: 0.76rem; color: var(--text-dim); background: transparent; border: 1px solid var(--border); border-radius: 14px; } .story-earlier button:hover { color: var(--accent-bright); border-color: var(--border-bright); } .thinking { display: flex; align-items: center; gap: 8px; font-family: var(--font-ui); font-size: 0.82rem; color: var(--text-dim); } .thinking i { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); animation: pulse 1.2s ease-in-out infinite; } .thinking i:nth-child(2) { animation-delay: 0.15s; } .thinking i:nth-child(3) { animation-delay: 0.3s; } @keyframes pulse { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } } /* ---------- Markdown in story prose ---------- */ .md-heading { font-family: var(--font-display); margin: 1.1em 0 0.4em; } .md-list { margin: 0 0 0.75em; padding-left: 1.4em; } .md-quote { margin: 0 0 0.75em; padding-left: 0.9em; border-left: 2px solid var(--border-bright); color: var(--text-dim); } .md-code { margin: 0 0 0.75em; padding: 10px 12px; overflow-x: auto; font-size: 0.85rem; background: var(--bg-input); border: 1px solid var(--border); border-radius: 5px; } .turn-text code { padding: 1px 4px; font-size: 0.88em; background: var(--bg-input); border-radius: 3px; } .md-link { text-decoration: underline dotted; text-underline-offset: 3px; } /* §75: the address is shown, and nothing is fetched. */ .md-image-blocked, .md-link-blocked { display: inline-block; padding: 1px 7px; font-family: var(--font-ui); font-size: 0.76rem; color: var(--text-dim); background: var(--bg-input); border: 1px dashed var(--border-bright); border-radius: 4px; } /* ---------- Composer ---------- */ .composer { position: sticky; bottom: 0; z-index: 15; width: min(66ch, 100%); margin: 0 auto; padding: 10px 24px calc(16px + env(keyboard-inset-height, 0px)); background: linear-gradient(to top, var(--bg) 62%, transparent); } .story-controls { display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; align-items: center; } /* M11: where the reader is in the story (post-M8 finding B). Pushed to the end of the control row so it reads as a status rather than another button, and quiet enough that it is not competing with the prose. */ .story-position { margin: 0 0 0 auto; font-size: 0.78rem; color: var(--text-dim); white-space: nowrap; } .story-position .moment { color: var(--text); } .story-position .ahead { color: var(--accent); } .story-controls button { padding: 5px 13px; font-size: 0.78rem; color: var(--text-dim); background: var(--bg-panel); border: 1px solid var(--border); border-radius: 5px; } .story-controls button:hover:not(:disabled) { color: var(--accent-bright); border-color: var(--border-bright); } .story-controls button:disabled { opacity: 0.35; cursor: default; } .input-bar { border: 1px solid var(--border-bright); border-radius: 9px; background: var(--bg-panel); transition: border-color 0.15s, box-shadow 0.15s; } .input-bar:focus-within { border-color: var(--accent-dim); box-shadow: 0 0 0 3px var(--accent-glow); } /* §13: while direction is on, the box must not look like the story box. */ .input-bar.directing { border-color: var(--accent); background: rgba(212, 169, 78, 0.06); } .input-row { display: flex; align-items: center; gap: 10px; padding: 7px 12px 0; font-family: var(--font-ui); font-size: 0.74rem; } .direction-toggle { display: flex; align-items: center; gap: 6px; color: var(--text-dim); cursor: pointer; } .direction-toggle input { margin: 0; } .input-bar.directing .direction-toggle { color: var(--accent-bright); } .direction-hint { color: var(--text-dim); font-style: italic; } .input-main { display: flex; align-items: flex-end; gap: 8px; padding: 4px 10px 10px; } .input-main textarea { flex: 1; /* forms.css gives every textarea a 70px floor, which is right for a form field and wrong for a composer that starts at one line and grows with what is typed. */ min-height: 0; max-height: 9em; padding: 6px 4px; font-family: var(--font-story); font-size: 1.05rem; line-height: 1.55; color: var(--text); background: transparent; border: none; resize: none; } .input-main textarea:focus { outline: none; } /* Reserved for a later version. Disabled, and visibly so. */ .dictate-reserved { padding: 5px 9px; font-size: 0.9rem; color: var(--text-dim); background: transparent; border: 1px dashed var(--border); border-radius: 6px; opacity: 0.4; cursor: not-allowed; } /* ---------- Failure ---------- */ .failure { width: min(66ch, 100%); margin: 0 auto 12px; padding: 12px 16px; font-family: var(--font-ui); font-size: 0.84rem; background: var(--bg-panel); border: 1px solid var(--danger); border-left-width: 3px; border-radius: 7px; } .failure-knowledge, .failure-state { border-color: var(--accent-dim); } .failure-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } .failure-head strong { flex: 1; } .failure-icon { color: var(--danger); } .failure-close { padding: 0 5px; color: var(--text-dim); background: transparent; border: none; } .failure-hint, .failure-kept { margin: 0 0 6px; color: var(--text-dim); line-height: 1.55; } .failure-kept { font-style: italic; } .failure details { margin-bottom: 6px; } .failure summary { cursor: pointer; color: var(--text-dim); font-size: 0.78rem; } .failure pre, .failure blockquote { margin: 6px 0 0; padding: 8px 10px; max-height: 12em; overflow: auto; font-size: 0.76rem; white-space: pre-wrap; background: var(--bg-input); border-radius: 5px; } .failure-actions { display: flex; gap: 8px; margin-top: 8px; } /* The model-setup guidance, when it appears above the composer. Same measure as the prose, so it reads as part of the column rather than a banner across it. */ .story-blocked { width: min(66ch, 100%); margin: 0 auto 12px; padding: 0 24px; }