flow.css (12277B)
1 /* ============================================================================ 2 Flow — native, on-theme flowcharts. 3 4 These replace the old ```mermaid fences, which never rendered (Shiki has no 5 mermaid grammar, so a fence printed its own source as a dead code listing). 6 Nothing here is JavaScript and nothing fetches: a flow is hand-authored HTML 7 (or inline SVG) drawn straight onto the page at build time. 8 9 A flow reads as *terminal art*: the same dark plate the code panes ride, in 10 both site themes, so a diagram sits in the same register as the listings it 11 sits between (see prose.css — "terminal output pasted into a printed 12 document"). The wrapper carries `.plate` (pins the Rosé Pine night palette 13 for its subtree) and `.corners` (the cyberdeck brackets), exactly like a 14 code pane, so every accent below — `--foam`, `--iris`, `--gold`, `--love`, 15 `--pine` — resolves to its night value on the cream page without a single 16 hardcoded hex. 17 18 Two primitives, one look: 19 • CSS-flow — ranks of `.flow-node` boxes joined by `.flow-edge` 20 connectors. Flexbox does the layout; there are no coordinates to get 21 wrong. Use it for linear chains, decision fan-outs and merges. 22 • SVG-flow — an inline `<svg class="flow-svg">` using the shared node / 23 edge / label classes. Use it only when the graph has back-edges, skip 24 edges or many-into-one merges that a rank layout would misdraw. 25 A `.flow-node` box and an `.fnode` rect share the same fill, hairline and 26 font, so a reader can't tell which engine drew a given box. 27 ========================================================================== */ 28 29 /* ---- Wrapper ------------------------------------------------------------- */ 30 .prose .flow { 31 position: relative; 32 margin-block: 1.7rem; 33 max-width: min(100%, 94ch); 34 background: var(--plate); 35 border: 0; 36 overflow: hidden; 37 color: var(--fg); 38 } 39 /* Runs to the prose measure; a wide flow scrolls inside its own body rather 40 than pushing the page. */ 41 .prose .flow > * { position: relative; z-index: 2; } 42 .prose .flow > .scanlines { z-index: 1; opacity: 0.5; } 43 44 /* The caption behaves like a code pane's titlebar: a kind mark at the left, 45 the flow direction at the right, both in the 9.5px terminal mono. */ 46 .prose .flow__cap { 47 display: flex; 48 align-items: center; 49 justify-content: space-between; 50 gap: 1rem; 51 padding: 0.7rem 1.15rem; 52 border-bottom: 1px solid var(--rule); 53 font-family: var(--font-term); 54 font-size: 9.5px; 55 letter-spacing: 0.18em; 56 text-transform: uppercase; 57 line-height: 1; 58 } 59 .prose .flow__cap .flow__kind { color: var(--iris); } 60 .prose .flow__cap .flow__kind::before { content: '◇ '; opacity: 0.8; } 61 .prose .flow__cap .flow__dir { color: var(--fg-faint); } 62 63 .prose .flow__body { 64 padding: clamp(1.05rem, 3.2vw, 1.9rem); 65 overflow-x: auto; 66 } 67 68 /* ---- CSS-flow: ranks + edges -------------------------------------------- */ 69 .prose .flow__diagram { 70 display: flex; 71 align-items: center; 72 justify-content: center; 73 gap: 0; 74 min-width: min-content; 75 } 76 .prose .flow__diagram[data-dir='td'] { flex-direction: column; } 77 .prose .flow__diagram[data-dir='lr'] { flex-direction: row; flex-wrap: wrap; } 78 79 /* A rank is one layer of the graph — a row (TD) or column (LR) of sibling 80 nodes that sit at the same depth. */ 81 .prose .flow-rank { 82 display: flex; 83 gap: clamp(0.7rem, 2.4vw, 1.6rem); 84 align-items: stretch; 85 justify-content: center; 86 } 87 .prose .flow__diagram[data-dir='lr'] .flow-rank { flex-direction: column; } 88 89 /* ---- Node --------------------------------------------------------------- */ 90 .prose .flow-node { 91 position: relative; 92 display: flex; 93 flex-direction: column; 94 align-items: center; 95 justify-content: center; 96 gap: 0.15rem; 97 text-align: center; 98 min-width: 8.5rem; 99 max-width: 16rem; 100 padding: 0.6rem 0.9rem; 101 background: var(--surface); 102 border: 1px solid var(--rule-hi); 103 color: var(--text); 104 font-family: var(--font-term); 105 font-size: 12.5px; 106 line-height: 1.32; 107 } 108 /* The second line of a node — what was `<br/>` in the mermaid source — reads 109 as a caption under the label. Mark it up as `<span class="sub">`. */ 110 .prose .flow-node .sub { 111 color: var(--subtle); 112 font-size: 10.5px; 113 letter-spacing: 0.01em; 114 } 115 .prose .flow-node code { 116 font-family: var(--font-term); 117 font-size: 0.95em; 118 background: none; 119 border: 0; 120 padding: 0; 121 color: var(--foam); 122 } 123 124 /* Node roles — colour keyed the same way the SVG kit keys `.fnode` variants. */ 125 .prose .flow-node.is-entry { border-color: var(--iris); } 126 .prose .flow-node.is-entry::before { 127 content: 'START'; 128 font-size: 8px; letter-spacing: 0.2em; color: var(--iris); 129 margin-bottom: 0.1rem; 130 } 131 .prose .flow-node.is-goal { 132 background: color-mix(in srgb, var(--pine) 32%, var(--surface)); 133 border-color: var(--foam); 134 color: var(--text); 135 } 136 .prose .flow-node.is-decision { border-color: var(--gold); } 137 .prose .flow-node.is-decision::before { 138 content: '◆ DECIDE'; 139 font-size: 8px; letter-spacing: 0.18em; color: var(--gold); 140 margin-bottom: 0.15rem; 141 } 142 .prose .flow-node.is-note { 143 background: var(--overlay); 144 border-style: dashed; 145 color: var(--subtle); 146 } 147 .prose .flow-node.is-danger { border-color: var(--love); } 148 .prose .flow-node.is-danger::before { 149 content: '! '; 150 color: var(--love); 151 } 152 153 /* ---- Edge (connector) --------------------------------------------------- */ 154 .prose .flow-edge { 155 position: relative; 156 align-self: center; 157 flex: none; 158 --edge: var(--foam); 159 } 160 /* Vertical connector (TD): a hairline with a triangle arrowhead. */ 161 .prose .flow__diagram[data-dir='td'] > .flow-edge, 162 .prose .flow-lane > .flow-edge { 163 width: 1px; 164 height: 2.3rem; 165 background: var(--edge); 166 margin: 0.05rem 0; 167 } 168 .prose .flow__diagram[data-dir='td'] > .flow-edge::after, 169 .prose .flow-lane > .flow-edge::after { 170 content: ''; 171 position: absolute; 172 left: 50%; bottom: -1px; 173 transform: translateX(-50%); 174 border-left: 4px solid transparent; 175 border-right: 4px solid transparent; 176 border-top: 6px solid var(--edge); 177 } 178 /* Horizontal connector (LR). */ 179 .prose .flow__diagram[data-dir='lr'] > .flow-edge { 180 height: 1px; 181 width: clamp(1.8rem, 5vw, 3rem); 182 background: var(--edge); 183 margin: 0 0.05rem; 184 } 185 .prose .flow__diagram[data-dir='lr'] > .flow-edge::after { 186 content: ''; 187 position: absolute; 188 top: 50%; right: -1px; 189 transform: translateY(-50%); 190 border-top: 4px solid transparent; 191 border-bottom: 4px solid transparent; 192 border-left: 6px solid var(--edge); 193 } 194 /* The label a mermaid `-->|text|` edge carried — a knockout chip centred on 195 the connector so the line reads through it. */ 196 .prose .flow-edge__label { 197 position: absolute; 198 left: 50%; top: 50%; 199 transform: translate(-50%, -50%); 200 background: var(--plate); 201 border: 1px solid var(--rule); 202 color: var(--foam); 203 font-family: var(--font-term); 204 font-size: 9.5px; 205 letter-spacing: 0.04em; 206 line-height: 1.1; 207 padding: 0.1rem 0.4rem; 208 white-space: nowrap; 209 z-index: 3; 210 } 211 .prose .flow-edge.is-back { --edge: var(--love); background: none; } 212 .prose .flow-edge.is-back.is-vert, 213 .prose .flow__diagram[data-dir='td'] > .flow-edge.is-back { border-left: 1px dashed var(--love); width: 0; background: none; } 214 .prose .flow-edge.is-dotted { background: none; border-top: 1px dashed var(--foam); } 215 216 /* ---- Branch / merge ----------------------------------------------------- */ 217 /* A decision fans out into lanes; each lane is its own little TD sub-flow 218 (labelled edge → node → …). */ 219 .prose .flow-branches { 220 display: flex; 221 gap: clamp(0.9rem, 4vw, 2.6rem); 222 align-items: flex-start; 223 justify-content: center; 224 } 225 .prose .flow-lane { 226 display: flex; 227 flex-direction: column; 228 align-items: center; 229 } 230 /* A join bar collects several lanes back into one node: a hairline spanning 231 the lanes with a single arrow dropping to the shared node below. */ 232 .prose .flow-join { 233 position: relative; 234 align-self: stretch; 235 height: 1.6rem; 236 margin-top: 0.1rem; 237 /* Room for the drop line and arrowhead below; the diagram has no gap. */ 238 margin-bottom: 1.55rem; 239 border-top: 1px solid var(--foam); 240 border-left: 1px solid var(--foam); 241 border-right: 1px solid var(--foam); 242 } 243 .prose .flow-join::after { 244 content: ''; 245 position: absolute; 246 left: 50%; bottom: -1.5rem; 247 width: 1px; height: 1.5rem; 248 background: var(--foam); 249 transform: translateX(-50%); 250 } 251 /* Arrowhead where the gather-bracket drops into the shared node below. */ 252 .prose .flow-join::before { 253 content: ''; 254 position: absolute; 255 left: 50%; bottom: -1.55rem; 256 transform: translateX(-50%); 257 border-left: 4px solid transparent; 258 border-right: 4px solid transparent; 259 border-top: 6px solid var(--foam); 260 z-index: 1; 261 } 262 263 /* ---- SVG-flow ----------------------------------------------------------- */ 264 /* One inline <svg class="flow-svg"> per complex graph, drawn on an orthogonal 265 grid. Every colour comes from a token resolved in the `.plate` subtree, so 266 the diagram flips with the theme like everything else. */ 267 .prose .flow-svg { 268 display: block; 269 width: 100%; 270 height: auto; 271 max-width: 100%; 272 min-width: 0; 273 font-family: var(--font-term); 274 overflow: visible; 275 } 276 .prose .flow-svg text { fill: var(--text); } 277 278 /* Node box + label. */ 279 .prose .flow-svg .fnode__box { 280 fill: var(--surface); 281 stroke: var(--rule-hi); 282 stroke-width: 1; 283 } 284 .prose .flow-svg .fnode__label { fill: var(--text); font-size: 13px; } 285 .prose .flow-svg .fnode__label .sub { fill: var(--subtle); font-size: 11px; } 286 287 /* Node roles mirror the CSS-flow ones. */ 288 .prose .flow-svg .is-entry .fnode__box { stroke: var(--iris); stroke-width: 1.4; } 289 .prose .flow-svg .is-goal .fnode__box { fill: color-mix(in srgb, var(--pine) 34%, var(--surface)); stroke: var(--foam); } 290 .prose .flow-svg .is-note .fnode__box { fill: var(--overlay); stroke: var(--rule); stroke-dasharray: 4 3; } 291 .prose .flow-svg .is-note .fnode__label { fill: var(--subtle); } 292 .prose .flow-svg .is-danger .fnode__box { stroke: var(--love); } 293 294 /* Decision diamond. */ 295 .prose .flow-svg .fdecision__poly { fill: var(--overlay); stroke: var(--gold); stroke-width: 1; } 296 .prose .flow-svg .fdecision__label { fill: var(--text); font-size: 12px; } 297 /* A text-heavy decision that won't fit a diamond: a gold-bordered rect node, 298 matching the CSS-flow `.is-decision` box. */ 299 .prose .flow-svg .is-decision .fnode__box { fill: var(--overlay); stroke: var(--gold); } 300 301 /* Edges. `context-stroke` on the arrowhead makes one shared marker take each 302 edge's own colour, so a foam forward edge and a love back-edge point in 303 their own ink from a single <marker> def. */ 304 .prose .flow-svg .fedge { 305 fill: none; 306 stroke: var(--foam); 307 stroke-width: 1.5; 308 } 309 .prose .flow-svg .fedge.is-back { stroke: var(--love); stroke-dasharray: 5 3; } 310 .prose .flow-svg .fedge.is-dotted { stroke: var(--foam); stroke-dasharray: 2 3; } 311 .prose .flow-svg .fedge.is-thick { stroke: var(--iris); stroke-width: 2.4; } 312 313 /* Edge label — a knockout chip like the CSS-flow one. */ 314 .prose .flow-svg .felabel__box { fill: var(--plate); stroke: var(--rule); stroke-width: 1; } 315 .prose .flow-svg .felabel__text { fill: var(--foam); font-size: 11px; } 316 317 /* The shared arrowhead marker lives once, hidden, in the sheet layout 318 (see Base.astro). These fallbacks apply if a diagram ships its own <defs>. */ 319 .prose .flow-svg .flow-arrow { fill: context-stroke; } 320 321 /* ---- Responsive --------------------------------------------------------- */ 322 @media (max-width: 620px) { 323 /* An LR chain becomes a TD stack on a phone: the row wraps and each edge 324 turns to point down. */ 325 .prose .flow__diagram[data-dir='lr'] { flex-direction: column; } 326 .prose .flow__diagram[data-dir='lr'] > .flow-edge { 327 width: 1px; 328 height: 1.9rem; 329 margin: 0.05rem 0; 330 } 331 .prose .flow__diagram[data-dir='lr'] > .flow-edge::after { 332 top: auto; right: auto; 333 left: 50%; bottom: -1px; 334 transform: translateX(-50%); 335 border-left: 4px solid transparent; 336 border-right: 4px solid transparent; 337 border-top: 6px solid var(--edge); 338 border-bottom: 0; 339 } 340 .prose .flow-node { min-width: 7rem; font-size: 12px; } 341 } 342 343 @media (prefers-reduced-motion: no-preference) { 344 /* Motion is opt-in across the site; the flow is static furniture and adds 345 none of its own. */ 346 }