feat: improve presenter scanning and mobile navigation

This commit is contained in:
lda
2026-07-13 09:40:26 +07:00 Verified
parent 6feb336af1
commit c36542413e
12 changed files with 765 additions and 30 deletions
+2
View File
@@ -181,6 +181,8 @@ cumulative timing, keeps optional detail/evidence/Q&A collapsed, and links to
the corresponding audience slide in a new tab. It performs no workflow RPC, the corresponding audience slide in a new tab. It performs no workflow RPC,
replay, live-target, or cross-window synchronization. Use ArrowLeft and replay, live-target, or cross-window synchronization. Use ArrowLeft and
ArrowRight to move between notes; covered checkboxes remain local to the page. ArrowRight to move between notes; covered checkboxes remain local to the page.
Must-say notes support authored inline Markdown emphasis for rapid scanning, and
the stable Previous/Next bar remains available at narrow viewport widths.
The final presentation beats frame the evaluation as bounded evidence, make The final presentation beats frame the evaluation as bounded evidence, make
claim boundaries and future work explicit, and end on the canonical defense claim boundaries and future work explicit, and end on the canonical defense
+1
View File
@@ -24,6 +24,7 @@
"radix-ui": "^1.6.2", "radix-ui": "^1.6.2",
"react": "19.2.7", "react": "19.2.7",
"react-dom": "19.2.7", "react-dom": "19.2.7",
"react-markdown": "10.1.0",
"react-router-dom": "^7.18.1", "react-router-dom": "^7.18.1",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"tw-shimmer": "^0.4.11", "tw-shimmer": "^0.4.11",
+2 -2
View File
@@ -197,7 +197,7 @@ describe("App", () => {
expect(screen.queryByLabelText("Lifecycle Explorer")).toBeNull(); expect(screen.queryByLabelText("Lifecycle Explorer")).toBeNull();
}); });
it("routes to read-only presenter notes separately from presentation mode", () => { it("routes to read-only presenter notes separately from presentation mode", async () => {
window.location.hash = "#scene/thesis/title"; window.location.hash = "#scene/thesis/title";
render( render(
<MemoryRouter initialEntries={["/presenter"]}> <MemoryRouter initialEntries={["/presenter"]}>
@@ -205,7 +205,7 @@ describe("App", () => {
</MemoryRouter>, </MemoryRouter>,
); );
expect(screen.getByRole("main", { name: /lda.chat presenter notes/i })).toBeInTheDocument(); expect(await screen.findByRole("main", { name: /lda.chat presenter notes/i })).toBeInTheDocument();
expect(screen.queryByRole("main", { name: /lda.chat presentation/i })).not.toBeInTheDocument(); expect(screen.queryByRole("main", { name: /lda.chat presentation/i })).not.toBeInTheDocument();
}); });
}); });
+6 -2
View File
@@ -1,14 +1,18 @@
import { lazy, Suspense } from "react";
import { Navigate, Route, Routes } from "react-router-dom"; import { Navigate, Route, Routes } from "react-router-dom";
import { ConsoleHome } from "./ConsoleHome.js"; import { ConsoleHome } from "./ConsoleHome.js";
import { PresentationRoute } from "../presentation/PresentationRoute.js"; import { PresentationRoute } from "../presentation/PresentationRoute.js";
import { PresenterRoute } from "../presentation/presenter/PresenterRoute.js";
const PresenterRoute = lazy(() => import("../presentation/presenter/PresenterRoute.js").then((module) => ({
default: module.PresenterRoute,
})));
export const AppRoutes = () => ( export const AppRoutes = () => (
<Routes> <Routes>
<Route path="/" element={<ConsoleHome />} /> <Route path="/" element={<ConsoleHome />} />
<Route path="/console" element={<ConsoleHome />} /> <Route path="/console" element={<ConsoleHome />} />
<Route path="/present" element={<PresentationRoute />} /> <Route path="/present" element={<PresentationRoute />} />
<Route path="/presenter" element={<PresenterRoute />} /> <Route path="/presenter" element={<Suspense fallback={null}><PresenterRoute /></Suspense>} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
); );
@@ -0,0 +1,22 @@
import type { PresenterBeatNote } from "./presenter-notes.js";
import { presenterHashForNote } from "./presenter-navigation.js";
type PresenterNavigationBarProps = {
readonly currentIndex: number;
readonly total: number;
readonly previous: PresenterBeatNote | null;
readonly next: PresenterBeatNote | null;
};
const DirectionLink = ({ note, children }: { readonly note: PresenterBeatNote | null; readonly children: string }) =>
note
? <a href={presenterHashForNote(note)}>{children}</a>
: <span aria-disabled="true">{children}</span>;
export const PresenterNavigationBar = ({ currentIndex, total, previous, next }: PresenterNavigationBarProps) => (
<nav className="presenter-navigation" aria-label="Presenter note navigation">
<DirectionLink note={previous}> Previous</DirectionLink>
<span>{currentIndex + 1} / {total}</span>
<DirectionLink note={next}>Next </DirectionLink>
</nav>
);
@@ -28,7 +28,7 @@ export const PresenterNote = ({ note, cumulativeSeconds, next, covered, onCovere
<section className="presenter-note__say" aria-labelledby="presenter-say"> <section className="presenter-note__say" aria-labelledby="presenter-say">
<span id="presenter-say">Say</span> <span id="presenter-say">Say</span>
<p>{note.mustSay}</p> <div className="presenter-note__markdown"><ReactMarkdown>{note.mustSay}</ReactMarkdown></div>
</section> </section>
{note.warning && <aside className="presenter-note__warning"><strong>Warning</strong><p>{note.warning}</p></aside>} {note.warning && <aside className="presenter-note__warning"><strong>Warning</strong><p>{note.warning}</p></aside>}
@@ -60,10 +60,11 @@ export const PresenterNote = ({ note, cumulativeSeconds, next, covered, onCovere
{next && ( {next && (
<section className="presenter-note__next" aria-label="Next beat preview"> <section className="presenter-note__next" aria-label="Next beat preview">
<span>Next · {formatPresenterTime(next.targetSeconds)}</span> <span>Next · {formatPresenterTime(next.targetSeconds)}</span>
<p>{next.mustSay}</p> <div className="presenter-note__next-copy"><ReactMarkdown>{next.mustSay}</ReactMarkdown></div>
<a href={presenterHashForNote(next)}>Go to next beat</a> <a href={presenterHashForNote(next)}>Go to next beat</a>
</section> </section>
)} )}
</article> </article>
); );
}; };
import ReactMarkdown from "react-markdown";
@@ -14,6 +14,9 @@ describe("PresenterRoute", () => {
render(<PresenterRoute />); render(<PresenterRoute />);
expect(screen.getByRole("main", { name: /lda.chat presenter notes/i })).toBeInTheDocument(); expect(screen.getByRole("main", { name: /lda.chat presenter notes/i })).toBeInTheDocument();
expect(screen.getByText(/This project began with the goal/i)).toBeInTheDocument(); expect(screen.getByText(/This project began with the goal/i)).toBeInTheDocument();
expect(screen.getByText("the system underneath the chat").tagName).toBe("STRONG");
expect(screen.getByRole("navigation", { name: /presenter note navigation/i })).toHaveTextContent("1 / 42");
expect(screen.getByRole("link", { name: "Next →" })).toHaveAttribute("href", "#scene/thesis/substrate");
expect(screen.getByRole("link", { name: /open audience slide/i })).toHaveAttribute("href", "/present#scene/thesis/title"); expect(screen.getByRole("link", { name: /open audience slide/i })).toHaveAttribute("href", "/present#scene/thesis/title");
expect(screen.queryByText(/live target/i)).not.toBeInTheDocument(); expect(screen.queryByText(/live target/i)).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /run prepared workflow/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /run prepared workflow/i })).not.toBeInTheDocument();
@@ -1,7 +1,9 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { findDiscussionBranch } from "../storyboard.js"; import { findDiscussionBranch } from "../storyboard.js";
import { PresenterNote } from "./PresenterNote.js"; import { PresenterNote } from "./PresenterNote.js";
import { PresenterNavigationBar } from "./PresenterNavigationBar.js";
import { PresenterShell } from "./PresenterShell.js"; import { PresenterShell } from "./PresenterShell.js";
import { presenterNotes } from "./presenter-notes.js";
import { presenterHashForNote, presenterNavigationFromHash } from "./presenter-navigation.js"; import { presenterHashForNote, presenterNavigationFromHash } from "./presenter-navigation.js";
import "./presenter.css"; import "./presenter.css";
@@ -34,7 +36,14 @@ export const PresenterRoute = () => {
return ( return (
<PresenterShell current={navigation.note} covered={covered}> <PresenterShell current={navigation.note} covered={covered}>
<div className="presenter-route__help" aria-label="Presenter keyboard help"> previous · next · disclosures stay local</div> {navigation.note && (
<PresenterNavigationBar
currentIndex={navigation.index}
total={presenterNotes.length}
previous={navigation.previous}
next={navigation.next}
/>
)}
{navigation.note && ( {navigation.note && (
<PresenterNote <PresenterNote
note={navigation.note} note={navigation.note}
@@ -60,7 +60,10 @@ describe("presenter note catalog", () => {
); );
for (const note of presenterNotes) { for (const note of presenterNotes) {
expect(speech, `${note.sceneId}/${note.beatId}`).toContain(note.mustSay); // Markdown emphasis is presenter-only typography; the runbook stores the
// same spoken words without requiring identical inline formatting.
const spokenText = note.mustSay.replaceAll("**", "");
expect(speech, `${note.sceneId}/${note.beatId}`).toContain(spokenText);
} }
}); });
@@ -53,7 +53,7 @@ export const presenterNotes = [
"thesis", "thesis",
"title", "title",
22, 22,
"This project began with the goal in the title: an AI agent for creating and automating workspace workflows. The difficult engineering problem became the system underneath the chat.", "This project began with the goal in the title: **an AI agent for creating and automating workspace workflows**. The difficult engineering problem became **the system underneath the chat**.",
["Thesis Abstract and Introduction"], ["Thesis Abstract and Introduction"],
{ {
warning: "Do not present the submitted system as a bundled autonomous planner.", warning: "Do not present the submitted system as a bundled autonomous planner.",
@@ -64,7 +64,7 @@ export const presenterNotes = [
"thesis", "thesis",
"substrate", "substrate",
23, 23,
"The submitted contribution is a typed workflow substrate: an external planner can propose work while the platform owns definitions, validation, bindings, execution records, traces, and explicit resume boundaries.", "The submitted contribution is a **typed workflow substrate**: an external planner can propose work while **the platform owns definitions, validation, bindings, execution records, traces, and explicit resume boundaries**.",
["Thesis Abstract and Introduction", "Thesis Contributions"], ["Thesis Abstract and Introduction", "Thesis Contributions"],
{ qnaBranchIds: ["where-is-ai-agent", "not-just-cli"] }, { qnaBranchIds: ["where-is-ai-agent", "not-just-cli"] },
), ),
@@ -72,7 +72,7 @@ export const presenterNotes = [
"problem", "problem",
"direct-actions", "direct-actions",
22, 22,
"A model can call tools and complete one task, but a tool transcript is not reusable automation.", "A model can call tools and complete one task, but **a tool transcript is not reusable automation**.",
["Thesis Problem Statement and Requirements"], ["Thesis Problem Statement and Requirements"],
{ qnaBranchIds: ["direct-orchestration", "not-just-scripts"] }, { qnaBranchIds: ["direct-orchestration", "not-just-scripts"] },
), ),
@@ -80,7 +80,7 @@ export const presenterNotes = [
"problem", "problem",
"missing-contracts", "missing-contracts",
23, 23,
"Reuse needs schemas, source bindings, persistence, traces, and declared recovery boundaries, with planning kept separate from execution.", "Reuse needs **schemas, source bindings, persistence, traces, and declared recovery boundaries**, with **planning kept separate from execution**.",
["Thesis Problem Statement and Requirements"], ["Thesis Problem Statement and Requirements"],
{ qnaBranchIds: ["why-schemas", "run-persistence"] }, { qnaBranchIds: ["why-schemas", "run-persistence"] },
), ),
@@ -104,7 +104,7 @@ export const presenterNotes = [
"planner-runtime", "planner-runtime",
"planner", "planner",
18, 18,
"An external model or human proposes and revises workflow structure; this keeps planning outside the runtime.", "An **external model or human proposes and revises workflow structure**; this keeps planning outside the runtime.",
["Thesis Architecture Overview"], ["Thesis Architecture Overview"],
{ qnaBranchIds: ["where-is-ai-agent", "not-just-cli"] }, { qnaBranchIds: ["where-is-ai-agent", "not-just-cli"] },
), ),
@@ -112,7 +112,7 @@ export const presenterNotes = [
"planner-runtime", "planner-runtime",
"runtime", "runtime",
18, 18,
"For fixed definitions and handler results, the runtime validates the graph, resolves sources, executes steps, records state and traces, and resumes only at declared boundaries.", "For **fixed definitions and handler results**, the runtime validates the graph, resolves sources, executes steps, records state and traces, and **resumes only at declared boundaries**.",
["Thesis Workflow Core", "Thesis Architecture Overview"], ["Thesis Workflow Core", "Thesis Architecture Overview"],
{ warning: "Qualify determinism; provider code, resource reads, and external side effects can vary.", qnaBranchIds: ["run-persistence", "typed-interrupts"] }, { warning: "Qualify determinism; provider code, resource reads, and external side effects can vary.", qnaBranchIds: ["run-persistence", "typed-interrupts"] },
), ),
@@ -120,7 +120,7 @@ export const presenterNotes = [
"planner-runtime", "planner-runtime",
"boundary", "boundary",
19, 19,
"Typed CLI and JSON-RPC operations reach the same Workflow API, making schemas, diagnostics, and lifecycle state machine-readable without importing runtime internals.", "**Typed CLI and JSON-RPC operations reach the same Workflow API**, making schemas, diagnostics, and lifecycle state machine-readable without importing runtime internals.",
["Thesis Architecture Overview", "docs/source_architecture.md"], ["Thesis Architecture Overview", "docs/source_architecture.md"],
{ qnaBranchIds: ["not-just-cli"] }, { qnaBranchIds: ["not-just-cli"] },
), ),
@@ -128,7 +128,7 @@ export const presenterNotes = [
"lifecycle", "lifecycle",
"draft", "draft",
11, 11,
"Draft is mutable authoring state.", "**Draft** is mutable authoring state.",
["Thesis Workflow Lifecycle"], ["Thesis Workflow Lifecycle"],
{ optionalDetail: "Raw plans can also create artifacts without passing through a Draft." }, { optionalDetail: "Raw plans can also create artifacts without passing through a Draft." },
), ),
@@ -136,21 +136,21 @@ export const presenterNotes = [
"lifecycle", "lifecycle",
"artifact", "artifact",
11, 11,
"Artifact is an immutable workflow definition.", "**Artifact** is an immutable workflow definition.",
["Thesis Workflow Lifecycle"], ["Thesis Workflow Lifecycle"],
), ),
beatNote( beatNote(
"lifecycle", "lifecycle",
"deployment", "deployment",
11, 11,
"Deployment binds an artifact version to concrete sources and runtime context.", "**Deployment** binds an artifact version to concrete sources and runtime context.",
["Thesis Workflow Lifecycle"], ["Thesis Workflow Lifecycle"],
), ),
beatNote( beatNote(
"lifecycle", "lifecycle",
"run", "run",
12, 12,
"Run records one execution, including status, diagnostics, output, trace, and an explicit stopped or interrupted state.", "**Run** records one execution, including status, diagnostics, output, trace, and an explicit stopped or interrupted state.",
["Thesis Workflow Lifecycle"], ["Thesis Workflow Lifecycle"],
{ qnaBranchIds: ["lifecycle-states", "run-persistence"] }, { qnaBranchIds: ["lifecycle-states", "run-persistence"] },
), ),
@@ -220,7 +220,7 @@ export const presenterNotes = [
"agent-handoff", "agent-handoff",
"request", "request",
20, 20,
"I will now show a prepared demonstration built on this platform. The chat is a presentation interface, not the autonomous planner evaluated by the thesis. The chat translates a report request into the same public lifecycle operations an external agent could call. This prepared path demonstrates product behavior and recorded evidence, not a fresh model-performance result.", "I will now show a **prepared demonstration built on this platform**. The chat is a presentation interface, **not the autonomous planner evaluated by the thesis**. The chat translates a report request into the same public lifecycle operations an external agent could call. This prepared path demonstrates product behavior and recorded evidence, not a fresh model-performance result.",
["Constrained demo agent and prepared replay recipe"], ["Constrained demo agent and prepared replay recipe"],
{ {
fallback: "This is the reviewed recording, not a live model planning this workflow.", fallback: "This is the reviewed recording, not a live model planning this workflow.",
@@ -276,7 +276,7 @@ export const presenterNotes = [
"run-from-deployment", "run-from-deployment",
"operation", "operation",
12, 12,
"The public workflow.runs.start operation validates the deployment and input, creates a persisted Run, and begins the reusable graph.", "The public **workflow.runs.start** operation validates the deployment and input, creates a **persisted Run**, and begins the reusable graph.",
["workflow.runs.start replay evidence"], ["workflow.runs.start replay evidence"],
{ qnaBranchIds: ["run-persistence"] }, { qnaBranchIds: ["run-persistence"] },
), ),
@@ -292,7 +292,7 @@ export const presenterNotes = [
"typed-human-boundary", "typed-human-boundary",
"interrupt", "interrupt",
15, 15,
"Execution pauses at a typed issue_review interrupt exposing request data, allowed outcomes, request schema, and resume schema.", "Execution pauses at a **typed issue_review interrupt** exposing request data, allowed outcomes, request schema, and resume schema.",
["Typed interrupt payload and resume contract"], ["Typed interrupt payload and resume contract"],
{ qnaBranchIds: ["typed-interrupts", "why-schemas"] }, { qnaBranchIds: ["typed-interrupts", "why-schemas"] },
), ),
@@ -308,7 +308,7 @@ export const presenterNotes = [
"resume-output-evidence", "resume-output-evidence",
"resume", "resume",
16, 16,
"On the submitted path, workflow.runs.resume continues the recorded interrupted Run.", "On the submitted path, **workflow.runs.resume continues the recorded interrupted Run**.",
["workflow.runs.resume replay evidence", "Revision replay identity"], ["workflow.runs.resume replay evidence", "Revision replay identity"],
{ fallback: "The submitted replay demonstrates same-run continuation; the revision branch is recorded separately.", qnaBranchIds: ["replay-provenance", "prepared-replay-boundary"] }, { fallback: "The submitted replay demonstrates same-run continuation; the revision branch is recorded separately.", qnaBranchIds: ["replay-provenance", "prepared-replay-boundary"] },
), ),
@@ -332,7 +332,7 @@ export const presenterNotes = [
"evaluation", "evaluation",
"cohort", "cohort",
40, 40,
"The evaluation combines conformance tests, deterministic case studies, and a manually audited external-agent campaign: 36 trials across two challenges, two hosted models, three instruction profiles, and three waves, with three attempts per cell.", "The evaluation combines conformance tests, deterministic case studies, and a **manually audited external-agent campaign**: 36 trials across two challenges, two hosted models, three instruction profiles, and three waves, with three attempts per cell.",
["Thesis Evaluation and Appendix C"], ["Thesis Evaluation and Appendix C"],
{ qnaBranchIds: ["evaluation-validity"] }, { qnaBranchIds: ["evaluation-validity"] },
), ),
@@ -348,7 +348,7 @@ export const presenterNotes = [
"evaluation", "evaluation",
"findings", "findings",
40, 40,
"Because prompts, product snapshots, and hosted conditions changed across waves, these results are longitudinal engineering evidence. They expose authoring and diagnostic gaps, not a benchmark of model success, token reduction, retry reduction, or superiority.", "Because prompts, product snapshots, and hosted conditions changed across waves, these results are **longitudinal engineering evidence**. They expose authoring and diagnostic gaps, **not a benchmark** of model success, token reduction, retry reduction, or superiority.",
["Thesis Evaluation and Appendix C", "Thesis Threats to Validity"], ["Thesis Evaluation and Appendix C", "Thesis Threats to Validity"],
{ warning: "Use non-benchmark wording; do not report the counts as general model performance.", qnaBranchIds: ["evaluation-validity"] }, { warning: "Use non-benchmark wording; do not report the counts as general model performance.", qnaBranchIds: ["evaluation-validity"] },
), ),
@@ -372,7 +372,7 @@ export const presenterNotes = [
"conclusion", "conclusion",
"conclusion", "conclusion",
20, 20,
"The contribution is architectural and implemented: external planners can propose workflows while a typed platform validates, binds, executes, persists, interrupts, resumes, and inspects them through public operations.", "The contribution is **architectural and implemented**: external planners can propose workflows while a typed platform **validates, binds, executes, persists, interrupts, resumes, and inspects** them through public operations.",
["Thesis Contributions", "Thesis Conclusion"], ["Thesis Contributions", "Thesis Conclusion"],
{ qnaBranchIds: ["where-is-ai-agent", "not-just-cli"] }, { qnaBranchIds: ["where-is-ai-agent", "not-just-cli"] },
), ),
@@ -29,7 +29,10 @@
.presenter-sidebar__beats a[data-covered="true"]:not([aria-current="page"]) { background: #dce9e2; } .presenter-sidebar__beats a[data-covered="true"]:not([aria-current="page"]) { background: #dce9e2; }
.presenter-route__reader { min-width: 0; padding: 1.25rem clamp(1.5rem, 5vw, 6rem) 5rem; } .presenter-route__reader { min-width: 0; padding: 1.25rem clamp(1.5rem, 5vw, 6rem) 5rem; }
.presenter-route__help { max-width: 72ch; margin: 0 auto 2rem; color: #67655f; font: 0.78rem "IBM Plex Mono", monospace; } .presenter-navigation { max-width: 72ch; min-height: 2.5rem; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; margin: 0 auto 2rem; border-bottom: 1px solid #d7d5d0; color: #67655f; font: 0.78rem "IBM Plex Mono", monospace; }
.presenter-navigation a { color: #155b49; font-weight: 650; text-decoration: none; }
.presenter-navigation a:last-child, .presenter-navigation > span:last-child { text-align: right; }
.presenter-navigation [aria-disabled="true"] { opacity: 0.35; }
.presenter-note, .presenter-qna { max-width: 72ch; margin: 0 auto; } .presenter-note, .presenter-qna { max-width: 72ch; margin: 0 auto; }
.presenter-note__header { display: flex; justify-content: space-between; gap: 2rem; padding-bottom: 1rem; border-bottom: 2px solid #20201e; } .presenter-note__header { display: flex; justify-content: space-between; gap: 2rem; padding-bottom: 1rem; border-bottom: 2px solid #20201e; }
.presenter-note__header span, .presenter-note__timing { color: #67655f; font: 0.78rem "IBM Plex Mono", monospace; } .presenter-note__header span, .presenter-note__timing { color: #67655f; font: 0.78rem "IBM Plex Mono", monospace; }
@@ -37,7 +40,8 @@
.presenter-note__timing { display: grid; align-content: start; gap: 0.25rem; text-align: right; } .presenter-note__timing { display: grid; align-content: start; gap: 0.25rem; text-align: right; }
.presenter-note__say { margin: 2rem 0; } .presenter-note__say { margin: 2rem 0; }
.presenter-note__say > span { color: #1e6b55; font: 700 0.8rem "IBM Plex Mono", monospace; text-transform: uppercase; } .presenter-note__say > span { color: #1e6b55; font: 700 0.8rem "IBM Plex Mono", monospace; text-transform: uppercase; }
.presenter-note__say p { margin: 0.55rem 0 0; font-size: 1.42rem; line-height: 1.58; text-wrap: pretty; } .presenter-note__markdown p { margin: 0.55rem 0 0; font-size: 1.42rem; line-height: 1.58; text-wrap: pretty; }
.presenter-note__markdown strong, .presenter-note__next-copy strong { color: #155b49; font-weight: 750; }
.presenter-note__warning, .presenter-note__fallback, .presenter-qna aside { margin: 1.2rem 0; padding: 0.8rem 0; border-block: 1px solid #a36919; } .presenter-note__warning, .presenter-note__fallback, .presenter-qna aside { margin: 1.2rem 0; padding: 0.8rem 0; border-block: 1px solid #a36919; }
.presenter-note__fallback { border-color: #3c6380; } .presenter-note__fallback { border-color: #3c6380; }
.presenter-note__warning p, .presenter-note__fallback p, .presenter-qna aside p { margin: 0.2rem 0 0; line-height: 1.5; } .presenter-note__warning p, .presenter-note__fallback p, .presenter-qna aside p { margin: 0.2rem 0 0; line-height: 1.5; }
@@ -48,7 +52,7 @@
.presenter-note__actions a, .presenter-note__next a, .presenter-qna a { color: #155b49; font-weight: 650; } .presenter-note__actions a, .presenter-note__next a, .presenter-qna a { color: #155b49; font-weight: 650; }
.presenter-note__next { margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid #a7a49e; color: #4f4d48; } .presenter-note__next { margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid #a7a49e; color: #4f4d48; }
.presenter-note__next > span { font: 0.75rem "IBM Plex Mono", monospace; text-transform: uppercase; } .presenter-note__next > span { font: 0.75rem "IBM Plex Mono", monospace; text-transform: uppercase; }
.presenter-note__next p { font-size: 1.05rem; line-height: 1.5; } .presenter-note__next-copy p { font-size: 1.05rem; line-height: 1.5; }
.presenter-qna > span { display: block; margin-top: 2rem; color: #67655f; font: 0.75rem "IBM Plex Mono", monospace; text-transform: uppercase; } .presenter-qna > span { display: block; margin-top: 2rem; color: #67655f; font: 0.75rem "IBM Plex Mono", monospace; text-transform: uppercase; }
.presenter-qna__short { font-size: 1.35rem; line-height: 1.55; } .presenter-qna__short { font-size: 1.35rem; line-height: 1.55; }
.presenter-qna dl { margin-top: 2rem; border-top: 1px solid #d7d5d0; padding-top: 1rem; } .presenter-qna dl { margin-top: 2rem; border-top: 1px solid #d7d5d0; padding-top: 1rem; }
@@ -59,11 +63,12 @@
.presenter-route { grid-template-columns: 1fr; } .presenter-route { grid-template-columns: 1fr; }
.presenter-sidebar { position: static; height: auto; max-height: 15rem; border-right: 0; border-bottom: 1px solid #d7d5d0; } .presenter-sidebar { position: static; height: auto; max-height: 15rem; border-right: 0; border-bottom: 1px solid #d7d5d0; }
.presenter-sidebar ol { grid-template-columns: repeat(2, minmax(0, 1fr)); } .presenter-sidebar ol { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.presenter-navigation { position: sticky; top: 0; z-index: 3; margin-inline: -0.5rem; padding-inline: 0.5rem; background: #f7f7f5; }
} }
@media print { @media print {
.presenter-route { display: block; background: white; } .presenter-route { display: block; background: white; }
.presenter-sidebar, .presenter-route__help, .presenter-note__actions { display: none; } .presenter-sidebar, .presenter-navigation, .presenter-note__actions { display: none; }
.presenter-route__reader { padding: 0; } .presenter-route__reader { padding: 0; }
.presenter-note { max-width: none; } .presenter-note { max-width: none; }
.presenter-note details { display: block; } .presenter-note details { display: block; }
+685
View File
File diff suppressed because it is too large Load Diff