| 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 |