| 1 | /* π€ carloku.com/docs β the documentation skin. |
| 2 | |
| 3 | Loaded after site.css, never instead of it. Every colour, measure, |
| 4 | type stack, focus ring, scrollbar and selection colour here comes |
| 5 | from that file's tokens, so both schemes come along for free: the |
| 6 | whole palette swaps under `prefers-color-scheme: dark`, which is the |
| 7 | one theme mechanism this site has. There is no [data-theme] switch to |
| 8 | match, and inventing a second, half-supported one for these pages |
| 9 | only would put the docs out of step with / and /get-started. |
| 10 | |
| 11 | What this file adds is the shape site.css has no page for β a |
| 12 | sidebar, a reference column, and fenced code. It stays inside the |
| 13 | house rules in DESIGN.md while it does it: |
| 14 | |
| 15 | - hairlines divide, boxes do not enclose (no bordered cards, and |
| 16 | no border heavier than 1px anywhere below); |
| 17 | - one purple carries links, focus and the you-are-here mark, over |
| 18 | an accent-soft wash β the same wash the code chips and the quiet |
| 19 | button's hover already use; |
| 20 | - a fence wears the terminal's clothes and stays dark in both |
| 21 | schemes, because anything terminal-shaped does (--term-*); |
| 22 | - mono carries commands and addresses, never prose; |
| 23 | - weights come off the variable axis at the tuned steps |
| 24 | (480/560/620/640/650/680), and tracking tightens as size grows. |
| 25 | |
| 26 | Same build rules as the rest of the site: nothing fetched, no |
| 27 | @import, no JavaScript. Motion is colour-only and short, so the |
| 28 | global prefers-reduced-motion kill switch in site.css covers it. */ |
| 29 | |
| 30 | /* ---------------------------------------------------------------- tokens -- */ |
| 31 | |
| 32 | :root { |
| 33 | /* The two measures site.css lacks, both computed from the real Geist |
| 34 | metrics rather than borrowed. |
| 35 | |
| 36 | Body copy sets at 1rem (site.css `body`). Geist's prose advance β |
| 37 | its letters weighted by English frequency, plus a space every 5.7 |
| 38 | characters β is 0.467em, so a 65-75 character line wants 30.4rem |
| 39 | to 35.0rem. --docs-measure sits near the top of that band at |
| 40 | roughly 73 characters, and is also site.css's existing lede width, |
| 41 | so the docs read at the same measure the rest of the site does. |
| 42 | |
| 43 | The column is wider than the measure because a reference page also |
| 44 | carries command lines. The longest in the corpus is 66 characters; |
| 45 | in Geist Mono (a true 0.6em advance) at the 0.88rem fences use, |
| 46 | that is 34.9rem, and 42rem of column leaves it 4.6rem of room |
| 47 | inside the fence's padding. So the ordinary command sits in its box |
| 48 | and only a genuinely long one has to scroll. 42rem is also what |
| 49 | site.css already caps /get-started's prose at. */ |
| 50 | --docs-measure: 34rem; |
| 51 | --docs-column: 42rem; |
| 52 | } |
| 53 | |
| 54 | /* ------------------------------------------------------------ page frame -- */ |
| 55 | |
| 56 | /* .docs-body and .language-sh carry nothing of their own, deliberately. |
| 57 | site.css's `body` rule already sets the ground, the type and the |
| 58 | voice; and the site ships no highlighter, so a fence's language class |
| 59 | has nothing to say. Inventing a declaration so the hooks appear in |
| 60 | this file would be worse than leaving them alone. */ |
| 61 | |
| 62 | /* Where you are, in words, above a hairline. The drawn wordmark and the |
| 63 | sticky translucent header belong to the pages that are a destination; |
| 64 | a reference page gets a line of text and gets out of the way. */ |
| 65 | .brandline { |
| 66 | display: flex; |
| 67 | align-items: baseline; |
| 68 | flex-wrap: wrap; |
| 69 | gap: 0.5rem; |
| 70 | max-width: var(--wrap); |
| 71 | margin-inline: auto; |
| 72 | padding: 1.3rem 1.5rem 0.85rem; |
| 73 | border-bottom: 1px solid var(--line); |
| 74 | font-size: 0.9rem; |
| 75 | color: var(--muted); |
| 76 | } |
| 77 | .brandline a { color: var(--muted); text-decoration: none; transition: color 120ms ease-out; } |
| 78 | .brandline a:hover { color: var(--accent); } |
| 79 | .brandline b { font-weight: 640; letter-spacing: -0.01em; color: var(--ink); } |
| 80 | .brandline a:hover b { color: inherit; } |
| 81 | .brandline .here { color: var(--ink); font-weight: 560; } |
| 82 | |
| 83 | /* The folio is an address, so it sets as data β mono, the way every |
| 84 | other identifier on this site does β and it is the first thing a |
| 85 | narrow viewport gives up. */ |
| 86 | .folio { |
| 87 | margin-left: auto; |
| 88 | font-family: var(--mono); |
| 89 | font-size: 0.78rem; |
| 90 | letter-spacing: 0.01em; |
| 91 | color: var(--faint); |
| 92 | } |
| 93 | @media (max-width: 40rem) { .folio { display: none; } } |
| 94 | |
| 95 | /* The skip link is site.css's .skip, whole: parked at top: -3rem and |
| 96 | slid into view on :focus-visible. It is the site's own idiom and it |
| 97 | already does what these pages need β with 40+ sidebar links ahead of |
| 98 | the article and no search to route around them, a keyboard user needs |
| 99 | the way past. Restating it here would only let the two drift. */ |
| 100 | |
| 101 | /* ----------------------------------------------------------------- shell -- */ |
| 102 | |
| 103 | .docs-shell { |
| 104 | max-width: var(--wrap); |
| 105 | margin-inline: auto; |
| 106 | padding: clamp(1.75rem, 4vw, 2.75rem) 1.5rem; |
| 107 | display: grid; |
| 108 | grid-template-columns: minmax(0, 1fr); |
| 109 | gap: 2.25rem; |
| 110 | align-items: start; |
| 111 | } |
| 112 | |
| 113 | /* Both children are grid items, so both get min-width: 0. Without it a |
| 114 | grid child's min-width: auto refuses to shrink below its widest |
| 115 | unbreakable content, and a long command line would scroll the page |
| 116 | sideways instead of scrolling inside its own fence. */ |
| 117 | .docs-shell > * { min-width: 0; } |
| 118 | |
| 119 | /* Two columns once there is room for both. Below this the nav keeps its |
| 120 | document order β it is written before the article β so it simply |
| 121 | stacks above the content, with no toggle and no script. */ |
| 122 | @media (min-width: 60rem) { |
| 123 | .docs-shell { |
| 124 | grid-template-columns: 15rem minmax(0, 1fr); |
| 125 | gap: clamp(2.5rem, 5vw, 4rem); |
| 126 | } |
| 127 | .docs-nav { |
| 128 | position: sticky; |
| 129 | top: 1.75rem; |
| 130 | max-height: calc(100vh - 3.5rem); |
| 131 | overflow-y: auto; |
| 132 | overscroll-behavior: contain; |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | /* ------------------------------------------------------------------- nav -- */ |
| 137 | |
| 138 | /* Every link is padded rather than pulled out with a negative margin: |
| 139 | the sidebar becomes a scroll container at the two-column breakpoint, |
| 140 | and an overhang inside `overflow-y: auto` would give the nav its own |
| 141 | horizontal scrollbar. The labels above are padded to match, so the |
| 142 | column still aligns with itself. */ |
| 143 | .docs-nav { font-size: 0.94rem; } |
| 144 | .docs-nav ul { list-style: none; padding: 0; } |
| 145 | .docs-nav li { margin-top: 0.1rem; } |
| 146 | |
| 147 | .docs-nav a { |
| 148 | display: block; |
| 149 | padding: 0.26rem 0.55rem; |
| 150 | border-radius: 0.4rem; |
| 151 | color: var(--muted); |
| 152 | font-weight: 480; |
| 153 | text-decoration: none; |
| 154 | transition: color 120ms ease-out, background 120ms ease-out; |
| 155 | } |
| 156 | .docs-nav a:hover { color: var(--ink); } |
| 157 | |
| 158 | /* The page you are on, marked with the accent over the accent-soft wash |
| 159 | β the same pair the code chips, the step numerals and the quiet |
| 160 | button's hover already use, so the mark is house vocabulary rather |
| 161 | than a new device. Keyed to both the class the template stamps and |
| 162 | the aria-current it stamps beside it, so the mark cannot outlive |
| 163 | either one. */ |
| 164 | .docs-nav a.active, |
| 165 | .docs-nav a[aria-current="page"] { |
| 166 | background: var(--accent-soft); |
| 167 | color: var(--accent); |
| 168 | font-weight: 560; |
| 169 | } |
| 170 | |
| 171 | .docs-nav-title { |
| 172 | margin-bottom: 1.15rem; |
| 173 | padding-inline: 0.55rem; |
| 174 | font-size: 1.05rem; |
| 175 | font-weight: 640; |
| 176 | letter-spacing: -0.01em; |
| 177 | } |
| 178 | .docs-nav-title a { color: var(--ink); font-weight: inherit; } |
| 179 | .docs-nav-title a:hover { color: var(--accent); } |
| 180 | |
| 181 | /* A section label, not a heading and not data: it stays in the voice |
| 182 | face at a quiet weight above a hairline. This site never sets tracked |
| 183 | small caps, and importing them here to say "documentation" would be |
| 184 | borrowing a convention rather than using ours. */ |
| 185 | .docs-nav-section { |
| 186 | margin-top: 1.6rem; |
| 187 | padding-top: 0.9rem; |
| 188 | margin-bottom: 0.5rem; |
| 189 | padding-inline: 0.55rem; |
| 190 | border-top: 1px solid var(--line); |
| 191 | font-size: 0.82rem; |
| 192 | font-weight: 560; |
| 193 | color: var(--faint); |
| 194 | } |
| 195 | .docs-nav-title + .docs-nav-section { margin-top: 0; padding-top: 0; border-top: 0; } |
| 196 | |
| 197 | /* The open page's own sections, indented under it against a hairline. */ |
| 198 | .docs-subnav { |
| 199 | margin: 0.35rem 0 0.7rem 0.85rem; |
| 200 | padding-left: 0.5rem; |
| 201 | border-left: 1px solid var(--line); |
| 202 | font-size: 0.875rem; |
| 203 | } |
| 204 | .docs-subnav li { margin-top: 0; } |
| 205 | .docs-subnav a { |
| 206 | padding: 0.16rem 0.45rem; |
| 207 | font-weight: 400; |
| 208 | color: var(--faint); |
| 209 | } |
| 210 | .docs-subnav a:hover { color: var(--accent); } |
| 211 | |
| 212 | /* --------------------------------------------------------------- article -- */ |
| 213 | |
| 214 | .docs-article { max-width: var(--docs-column); min-width: 0; } |
| 215 | |
| 216 | .docs-article h1 { |
| 217 | font-size: clamp(2rem, 4.4vw, 2.7rem); |
| 218 | line-height: 1.06; |
| 219 | letter-spacing: -0.032em; |
| 220 | font-weight: 680; |
| 221 | text-wrap: balance; |
| 222 | /* site.css's element-level h1/h2 rules carry a margin-bottom for the |
| 223 | landing page's rhythm. Here the following element owns the gap, so |
| 224 | the heading gives it up rather than adding to it. */ |
| 225 | margin-bottom: 0; |
| 226 | } |
| 227 | .docs-article h1 + p { |
| 228 | margin-top: 1.15rem; |
| 229 | font-size: 1.12rem; |
| 230 | line-height: 1.65; |
| 231 | max-width: var(--docs-measure); |
| 232 | } |
| 233 | |
| 234 | /* A group opens on a full hairline β the same divider /get-started uses |
| 235 | between its sections. The reference reads as a run of divided |
| 236 | entries, never as a stack of boxes. */ |
| 237 | .docs-article h2 { |
| 238 | margin-top: clamp(2.75rem, 5.5vw, 4rem); |
| 239 | padding-top: clamp(1.6rem, 3.2vw, 2.25rem); |
| 240 | border-top: 1px solid var(--line); |
| 241 | font-size: clamp(1.35rem, 2.4vw, 1.7rem); |
| 242 | line-height: 1.2; |
| 243 | letter-spacing: -0.025em; |
| 244 | font-weight: 650; |
| 245 | text-wrap: balance; |
| 246 | margin-bottom: 0; |
| 247 | /* No measure cap: the hairline is the divider, so it runs the width of |
| 248 | the column rather than stopping short of the fences beneath it. The |
| 249 | longest heading in the corpus is 37 characters β 29rem at this size |
| 250 | β so nothing wraps for want of the cap. */ |
| 251 | } |
| 252 | |
| 253 | /* h3/h4 and the block elements further down are the floor for markdown |
| 254 | this corpus does not use yet β it is authored in this repo and grows |
| 255 | by hand. Kept deliberately short: enough that a new page cannot land |
| 256 | unstyled, not so much that it becomes a theme nobody asked for. */ |
| 257 | .docs-article h3 { |
| 258 | margin-top: 2.2rem; |
| 259 | font-size: 1.08rem; |
| 260 | font-weight: 620; |
| 261 | letter-spacing: -0.01em; |
| 262 | color: var(--ink); |
| 263 | } |
| 264 | .docs-article h4 { |
| 265 | margin-top: 1.8rem; |
| 266 | font-size: 0.98rem; |
| 267 | font-weight: 620; |
| 268 | letter-spacing: -0.01em; |
| 269 | color: var(--ink); |
| 270 | } |
| 271 | |
| 272 | .docs-article :is(h1, h2, h3, h4) { scroll-margin-top: 1.5rem; } |
| 273 | |
| 274 | .docs-article p { |
| 275 | margin-top: 1.05rem; |
| 276 | max-width: var(--docs-measure); |
| 277 | color: var(--muted); |
| 278 | text-wrap: pretty; |
| 279 | } |
| 280 | .docs-article :is(h2, h3, h4) + p { margin-top: 0.85rem; } |
| 281 | .docs-article strong { color: var(--ink); font-weight: 620; } |
| 282 | .docs-article em { color: var(--ink); } |
| 283 | |
| 284 | .docs-article ul:not(.docs-cards), |
| 285 | .docs-article ol { |
| 286 | margin-top: 1rem; |
| 287 | padding-left: 1.2rem; |
| 288 | max-width: var(--docs-measure); |
| 289 | color: var(--muted); |
| 290 | } |
| 291 | /* Scoped with a child combinator rather than `.docs-article li + li`, |
| 292 | which would outrank `.docs-cards li` below and break the index |
| 293 | grid's row alignment. */ |
| 294 | .docs-article ul:not(.docs-cards) > li + li, |
| 295 | .docs-article ol > li + li { margin-top: 0.45rem; } |
| 296 | .docs-article blockquote { |
| 297 | margin-top: 1.15rem; |
| 298 | padding-left: 1.1rem; |
| 299 | border-left: 1px solid var(--line-strong); |
| 300 | max-width: var(--docs-measure); |
| 301 | color: var(--faint); |
| 302 | font-size: 0.95rem; |
| 303 | } |
| 304 | .docs-article hr { |
| 305 | margin-block: 2.25rem; |
| 306 | border: 0; |
| 307 | border-top: 1px solid var(--line); |
| 308 | } |
| 309 | .docs-article table { |
| 310 | margin-top: 1.25rem; |
| 311 | width: 100%; |
| 312 | border-collapse: collapse; |
| 313 | font-size: 0.95rem; |
| 314 | } |
| 315 | .docs-article :is(th, td) { |
| 316 | text-align: left; |
| 317 | padding: 0.6rem 0.9rem 0.6rem 0; |
| 318 | border-top: 1px solid var(--line); |
| 319 | color: var(--muted); |
| 320 | vertical-align: baseline; |
| 321 | } |
| 322 | .docs-article th { color: var(--ink); font-weight: 620; } |
| 323 | |
| 324 | .docs-article a { |
| 325 | color: var(--accent); |
| 326 | text-decoration: none; |
| 327 | } |
| 328 | .docs-article a:hover { text-decoration: underline; text-underline-offset: 0.2em; } |
| 329 | |
| 330 | /* ------------------------------------------------------------------ code -- */ |
| 331 | |
| 332 | /* A fence is a box that scrolls itself: the page must never scroll |
| 333 | sideways because a command was long. It wears the terminal's clothes |
| 334 | and keeps them in both schemes, under the rule that anything |
| 335 | terminal-shaped sits on Terminal Black β the same clothes |
| 336 | /get-started's blocks already wear, and the reason a Carloku page |
| 337 | reads as a Carloku page. */ |
| 338 | .docs-article pre { |
| 339 | margin: 1.15rem 0 0; |
| 340 | max-width: 100%; |
| 341 | min-width: 0; |
| 342 | overflow-x: auto; |
| 343 | overscroll-behavior-x: contain; |
| 344 | -webkit-overflow-scrolling: touch; |
| 345 | padding: 1rem 1.2rem; |
| 346 | border: 1px solid var(--term-line); |
| 347 | border-radius: 0.7rem; |
| 348 | background: var(--term-bg); |
| 349 | color: var(--term-ink); |
| 350 | font-family: var(--mono); |
| 351 | font-size: 0.88rem; |
| 352 | line-height: 1.7; |
| 353 | } |
| 354 | .docs-article p + pre { margin-top: 0.9rem; } |
| 355 | |
| 356 | /* Inline code is a chip: mono on an accent-soft wash, the way the |
| 357 | commands appear in prose everywhere else on this site. Inside a fence |
| 358 | that chip would tile the whole block, so it is undone there. */ |
| 359 | .docs-article code { |
| 360 | font-family: var(--mono); |
| 361 | font-size: 0.88em; |
| 362 | background: var(--accent-soft); |
| 363 | color: var(--accent-ink); |
| 364 | padding: 0.12em 0.4em; |
| 365 | border-radius: 0.3em; |
| 366 | } |
| 367 | .docs-article pre code { |
| 368 | display: block; |
| 369 | padding: 0; |
| 370 | border-radius: 0; |
| 371 | background: none; |
| 372 | color: inherit; |
| 373 | font-size: inherit; |
| 374 | white-space: pre; |
| 375 | } |
| 376 | .docs-article a code { color: inherit; } |
| 377 | |
| 378 | /* ------------------------------------------------------------- the index -- */ |
| 379 | |
| 380 | /* The index's entries: a label and a line saying what is inside. They |
| 381 | are not cards and they are not boxed β this site separates siblings |
| 382 | with a single hairline and whitespace, the way the feature list and |
| 383 | the guide's sections do. Two ruled columns of contents, nothing to |
| 384 | lift off the page. */ |
| 385 | .docs-cards { |
| 386 | margin-top: 1.25rem; |
| 387 | display: grid; |
| 388 | grid-template-columns: minmax(0, 1fr); |
| 389 | column-gap: 2.5rem; |
| 390 | list-style: none; |
| 391 | padding: 0; |
| 392 | } |
| 393 | @media (min-width: 46rem) { |
| 394 | .docs-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } |
| 395 | } |
| 396 | .docs-cards li { |
| 397 | min-width: 0; |
| 398 | margin-top: 0; |
| 399 | padding: 1.05rem 0 1.15rem; |
| 400 | border-top: 1px solid var(--line); |
| 401 | } |
| 402 | .docs-cards a { display: inline-block; text-decoration: none; } |
| 403 | .docs-cards a strong { |
| 404 | font-size: 1.05rem; |
| 405 | font-weight: 620; |
| 406 | letter-spacing: -0.01em; |
| 407 | color: var(--ink); |
| 408 | transition: color 120ms ease-out; |
| 409 | } |
| 410 | .docs-cards a:hover strong { color: var(--accent); } |
| 411 | .docs-cards a:hover { text-decoration: none; } |
| 412 | .docs-cards span { |
| 413 | display: block; |
| 414 | margin-top: 0.25rem; |
| 415 | font-size: 0.95rem; |
| 416 | color: var(--muted); |
| 417 | text-wrap: pretty; |
| 418 | } |
| 419 | |
| 420 | /* The index's h2s head a grid rather than prose, so they need less air |
| 421 | under them and no measure cap. */ |
| 422 | .docs-article h2 + .docs-cards { margin-top: 1.4rem; } |
| 423 | |
| 424 | /* ------------------------------------------------------------------ feet -- */ |
| 425 | |
| 426 | /* Qualified so it outranks `.docs-article p` β this is a <p> inside the |
| 427 | article, and the unqualified class would lose the colour and the top |
| 428 | margin to it. */ |
| 429 | .docs-article .docs-foot { |
| 430 | margin-top: clamp(2.75rem, 5.5vw, 4rem); |
| 431 | padding-top: 1.15rem; |
| 432 | border-top: 1px solid var(--line); |
| 433 | max-width: var(--docs-measure); |
| 434 | font-size: 0.9rem; |
| 435 | color: var(--faint); |
| 436 | } |
| 437 | .docs-article .docs-foot a { color: var(--faint); } |
| 438 | .docs-article .docs-foot a:hover { color: var(--accent); } |
| 439 | |
| 440 | /* site.css's <footer> already gives the rule, the muted voice and the |
| 441 | block padding; these pages have no .wrap element inside it, so the |
| 442 | measure and the gutters are what is left to supply. */ |
| 443 | .docs-sitefoot { |
| 444 | max-width: var(--wrap); |
| 445 | margin-inline: auto; |
| 446 | padding-inline: 1.5rem; |
| 447 | } |
| 448 | .docs-provenance { font-size: 0.85rem; color: var(--faint); } |
| 449 | .docs-provenance a { color: var(--faint); } |
| 450 | .docs-provenance a:hover { color: var(--accent); } |
| 451 | |