返回 html-ppt-skill
base.css
根目录 / assets / base.css
1 /* html-ppt :: base.css — reset + shared tokens + layout primitives */
2 /* Default tokens. Themes in assets/themes/*.css override the :root block. */
3 :root {
4 --bg: #ffffff;
5 --bg-soft: #f7f7f8;
6 --surface: #ffffff;
7 --surface-2: #f2f2f4;
8 --border: rgba(0,0,0,.08);
9 --border-strong: rgba(0,0,0,.16);
10 --text-1: #111216;
11 --text-2: #55596a;
12 --text-3: #8a8f9e;
13 --accent: #3b6cff;
14 /* Text drawn on top of an --accent fill. Every theme in assets/themes/
15 * and every full-deck style.css overrides this with whichever of white /
16 * near-black clears WCAG AA against its own accent — a hardcoded ink
17 * cannot, because accents here range from #ffffff to #000000.
18 * This default is black rather than white on purpose: against the default
19 * --accent #3b6cff, white measures 4.40:1 and black 4.77:1, so only black
20 * clears AA for normal text. Reached by any deck that links no theme. */
21 --accent-ink: #000000;
22 --accent-2: #7a5cff;
23 --accent-3: #ff5c8a;
24 --good: #1aaf6c;
25 --warn: #f5a524;
26 --bad: #e0445a;
27 --grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
28 --grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
29 --radius: 18px;
30 --radius-sm: 12px;
31 --radius-lg: 26px;
32 --shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
33 --shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
34 --font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
35 --font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
36 --font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
37 --font-display: var(--font-sans);
38 --letter-tight: -.03em;
39 --letter-normal: -.01em;
40 --ease: cubic-bezier(.4,0,.2,1);
41 }
42
43 *,*::before,*::after{box-sizing:border-box}
44 html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
45 font-family:var(--font-sans);font-weight:400;line-height:1.6;
46 -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
47 letter-spacing:var(--letter-normal)}
48
49 /* Viewport host: centres the fixed-size design canvas and letterboxes the rest.
50 Every px in a slide therefore means the same thing in the browser, in the
51 presenter preview, in the overview thumbnail and in a headless render. */
52 body{width:100vw;height:100vh;overflow:hidden;
53 display:flex;align-items:center;justify-content:center}
54 img,svg,video{max-width:100%;display:block}
55 a{color:var(--accent);text-decoration:none}
56 a:hover{text-decoration:underline}
57 code,kbd,pre,samp{font-family:var(--font-mono)}
58
59 /* ================= SLIDE SYSTEM ================= */
60 /* ================= DESIGN CANVAS =================
61 Slides are authored against a FIXED 1920x1080 canvas and scaled to fit the
62 viewport by runtime.js (--deck-scale). Before this, .deck was 100vw/100vh
63 while type and spacing stayed in absolute px, so a 1512x850 laptop window
64 and a 1920x1080 render produced visibly different pages (issue #20).
65
66 Escape hatches:
67 <body data-fit="fluid"> -> old behaviour, canvas follows the viewport
68 <div class="deck" data-w=".." data-h=".."> -> custom canvas (e.g. 1080x1440 for 3:4)
69 */
70 .deck{position:relative;flex:none;
71 width:var(--deck-w,1920px);height:var(--deck-h,1080px);
72 overflow:hidden;background:var(--bg);
73 transform:scale(var(--deck-scale,1));transform-origin:center center}
74
75 /* Opt out: fluid canvas, exactly what the deck did before. */
76 body[data-fit="fluid"]{display:block}
77 body[data-fit="fluid"] .deck{width:100vw;height:100vh;transform:none}
78 .slide{
79 position:absolute;inset:0;
80 display:flex;flex-direction:column;justify-content:center;
81 padding:72px 96px;
82 box-sizing:border-box;
83 opacity:0;pointer-events:none;
84 transition:opacity .5s var(--ease), transform .5s var(--ease);
85 transform:translateX(30px);
86 overflow:hidden;
87 }
88 .slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
89 .slide.is-prev{transform:translateX(-30px)}
90
91 /* single-page standalone (used when a layout file is opened directly) */
92 body.single .slide{position:relative;width:100%;height:100%;opacity:1;transform:none;pointer-events:auto}
93 body[data-fit="fluid"].single .slide{width:100vw;height:100vh}
94
95 /* ================= TYPOGRAPHY ================= */
96 .eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
97 .kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
98 h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
99 h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
100 h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
101 h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
102 .lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
103 .dim{color:var(--text-2)}
104 .dim2{color:var(--text-3)}
105 .mono{font-family:var(--font-mono)}
106 .serif{font-family:var(--font-serif)}
107 .gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
108
109 /* ================= LAYOUT PRIMITIVES ================= */
110 .stack>*+*{margin-top:14px}
111 .row{display:flex;gap:24px;align-items:center}
112 .row.wrap{flex-wrap:wrap}
113 .grid{display:grid;gap:24px}
114 .g2{grid-template-columns:repeat(2,1fr)}
115 .g3{grid-template-columns:repeat(3,1fr)}
116 .g4{grid-template-columns:repeat(4,1fr)}
117 .center{display:flex;align-items:center;justify-content:center;text-align:center}
118 .fill{flex:1}
119 .sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
120 .mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
121 .mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
122
123 /* ================= CARDS ================= */
124 .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
125 padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
126 .card-soft{background:var(--surface-2);border:1px solid var(--border)}
127 .card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
128 .card-accent{background:var(--surface);border-top:3px solid var(--accent)}
129 .card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
130 .card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
131
132 .pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
133 background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
134 .pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
135
136 /* ================= BARS / DIVIDERS ================= */
137 .divider{height:1px;background:var(--border);width:100%}
138 .divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
139
140 /* ================= IMAGES (issue #17) =================
141 * Framing primitives shared by every image-* layout. The frame owns the
142 * aspect ratio and the crop; the <img> just fills it. That's what lets you
143 * swap in a photo of any shape without touching the layout.
144 * <figure class="img-frame"><img src="…" alt=""></figure>
145 * .img-frame.contain -> letterbox instead of crop (screenshots, diagrams,
146 * logos — anything where cropping loses information)
147 * --img-ratio -> aspect ratio of the frame (default 16/10)
148 * --img-pos -> object-position, e.g. "top" to protect a headline
149 */
150 .img-frame{position:relative;margin:0;overflow:hidden;
151 border-radius:var(--radius);box-shadow:var(--shadow);background:var(--surface-2);
152 aspect-ratio:var(--img-ratio,16/10)}
153 .img-frame > img{width:100%;height:100%;object-fit:cover;object-position:var(--img-pos,center)}
154 .img-frame.contain{background:var(--bg-soft);box-shadow:none;border:1px solid var(--border)}
155 .img-frame.contain > img{object-fit:contain}
156 .img-frame.fill{height:100%;aspect-ratio:auto}
157 .img-scrim{position:absolute;inset:0;
158 background:linear-gradient(180deg,transparent 42%,rgba(8,10,20,.72))}
159 .img-cap{margin:10px 2px 0;font-size:14px;line-height:1.5;color:var(--text-3)}
160 .img-tag{position:absolute;top:14px;left:14px;padding:5px 12px;border-radius:999px;
161 background:rgba(10,12,20,.55);color:#fff;font-size:12px;letter-spacing:.06em;
162 text-transform:uppercase;backdrop-filter:blur(6px)}
163
164 /* ================= CHROME (header/footer/progress) ================= */
165 .deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
166 font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
167 .deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
168 font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
169 .slide-number::before{content:attr(data-current)}
170 .slide-number::after{content:" / " attr(data-total)}
171 .progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
172 .progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
173
174 /* ---- custom logo (issue #11) ----
175 * One logo per deck, drawn over the slide area. Two ways in, same CSS:
176 * 1. hand-author it -> <img class="deck-logo" data-pos="bottom-right" src="…">
177 * 2. declare it once -> <body data-logo="…" data-logo-position="bottom-right">
178 * (1) needs no JS at all; (2) is created by runtime.js. It lives inside .deck
179 * so it scales with the deck. On an exported PDF it is painted per page by the
180 * @media print block below, not by this element — see the note there. */
181 .deck-logo{position:absolute;z-index:11;pointer-events:none;
182 height:var(--logo-size,44px);width:auto;max-width:30%;object-fit:contain;
183 opacity:var(--logo-opacity,.9)}
184 .deck-logo[data-pos="top-left"] {top:var(--logo-inset-y,28px);left:var(--logo-inset-x,40px)}
185 .deck-logo[data-pos="top-right"] {top:var(--logo-inset-y,28px);right:var(--logo-inset-x,40px)}
186 .deck-logo[data-pos="bottom-left"] {bottom:var(--logo-inset-y,28px);left:var(--logo-inset-x,40px)}
187 .deck-logo[data-pos="bottom-right"] {bottom:var(--logo-inset-y,28px);right:var(--logo-inset-x,40px)}
188 .deck-logo:not([data-pos]) {top:28px;right:40px}
189
190 /* ================= PRESENTER / OVERVIEW ================= */
191 /* Keep this list in sync with NOTE_SEL in assets/runtime.js — the runtime
192 * reads speaker notes from all of these, so all of them must be hidden
193 * from the audience. */
194 .notes,.speaker-notes{display:none!important}
195 .notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
196 padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
197 transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
198 .notes-overlay.open{transform:translateY(0)}
199 .overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
200 display:none;padding:40px;overflow:auto}
201 .overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
202 .overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
203 aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
204 font-size:11px;transition:transform .2s var(--ease)}
205 .overview .thumb:hover{transform:scale(1.04)}
206 .overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
207 .overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
208
209 /* ================= PRESENTER VIEW ================= */
210 /* Presenter view opens in a separate popup window (S key).
211 * All presenter styles are self-contained in the popup HTML generated by runtime.js.
212 * The audience window (this file) is NOT affected — it stays as normal deck view.
213 * Only the .notes class below is needed to hide speaker notes from audience. */
214
215 /* ================= UTILITY ================= */
216 .hidden{display:none!important}
217 .nowrap{white-space:nowrap}
218 .tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
219 .uppercase{text-transform:uppercase;letter-spacing:.12em}
220
221 /* ================= PRINT ================= */
222 @media print{
223 /* Every declaration here is !important on purpose. A template stylesheet
224 * scopes its rules as `.tpl-name .slide`, which is specificity (0,2,0) and
225 * beats a bare `.slide` at (0,1,0) — so without !important a template
226 * silently keeps its screen layout in print. That is how dir-key-nav-minimal
227 * (position:absolute) printed 8 slides onto 1 page and xhs-post printed 9
228 * onto 2: only the opacity/transform declarations on this rule carried
229 * !important, and the ones that decide pagination did not. */
230 body{display:block!important;width:auto!important;height:auto!important;overflow:visible!important}
231 .deck{transform:none!important;width:100%!important;height:auto!important;
232 min-height:0!important;display:block!important}
233 .slide{position:relative!important;inset:auto!important;
234 opacity:1!important;transform:none!important;
235 page-break-after:always;break-after:page;
236 width:100%!important;height:100vh!important;max-height:100vh!important}
237 .slide:last-child{page-break-after:auto;break-after:auto}
238 .deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
239
240 /* Logo in print. The .deck-logo element is position:absolute inside .deck,
241 * and print collapses .deck to height:auto with every slide stacked — so the
242 * element can only anchor ONCE against the whole document and lands on a
243 * single page (or none). Hide it and paint the logo per slide instead: in
244 * print one .slide is exactly one page. runtime.js mirrors the image URL and
245 * placement onto .deck (--logo-print / data-logo-print), so this only kicks
246 * in when the runtime is present; a JS-less deck keeps the element. */
247 .deck[data-logo-print] > .deck-logo{display:none!important}
248 .deck[data-logo-print] .slide:not([data-no-logo])::after{
249 content:"";position:absolute;z-index:11;pointer-events:none;
250 width:30%;height:var(--logo-size,44px);
251 background-image:var(--logo-print);background-repeat:no-repeat;
252 background-size:contain;opacity:var(--logo-opacity,.9)}
253 .deck[data-logo-print="top-left"] .slide:not([data-no-logo])::after{top:var(--logo-inset-y,28px);left:var(--logo-inset-x,40px);background-position:left top}
254 .deck[data-logo-print="top-right"] .slide:not([data-no-logo])::after{top:var(--logo-inset-y,28px);right:var(--logo-inset-x,40px);background-position:right top}
255 .deck[data-logo-print="bottom-left"] .slide:not([data-no-logo])::after{bottom:var(--logo-inset-y,28px);left:var(--logo-inset-x,40px);background-position:left bottom}
256 .deck[data-logo-print="bottom-right"] .slide:not([data-no-logo])::after{bottom:var(--logo-inset-y,28px);right:var(--logo-inset-x,40px);background-position:right bottom}
257 }
258
258 lines CSS