From eed5348567b4999ac140c6b6107575ef0daa96fe Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Tue, 2 Jun 2026 09:12:38 -0400 Subject: [PATCH] =?UTF-8?q?css:=20grid-only=20layout=20=E2=80=94=20convert?= =?UTF-8?q?=20every=20flex=20container=20to=20grid=20+=20pin=20column=20wi?= =?UTF-8?q?dths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes in one — the immediate layout bug from the screenshot (timeline sliding into column 1 with controls eating ~80% of the viewport) and the architectural rule that all zebra page layout uses grid. Layout bug (was: when cameras-col gets .hidden + the global .hidden utility's display:none !important, the grid auto-placement promoted .timeline into column 1 and .controls into column 2 → controls took the 1fr middle track). Fix: - explicit grid-column: 1/2/3 on cameras-col / timeline / controls so each child stays in its assigned column regardless of siblings going display:none - .cameras-col uses 'display: grid !important' to override the global .hidden util, then only its content (h2 + #cameras) goes display:none via separate selectors when the .hidden class is present - controls track clamped to min(360px, 50vw) so a wide window can't let the side panel eat the screen-share area Grid-only refactor: - every flex container converted: .timeline, .cameras-col, .game-tabs, .screen-tile, .screen-meta, .tap-play, .camera-tile, .row, .mod-actions, .invite-banner, .invite-actions, .notice-banner - chat.html + zebra-audio.html same treatment (.row, .field-row, .share-box .copy-row, the inline H2 style, .mode-toggle, .dot) - inline 'style=flex:1' on inputs/meters in chat.html replaced with 'style=width:100%' - now zero 'display: flex' / 'inline-flex' across all five zebra pages - CLAUDE.md documents the grid-only rule under web-page authoring --- CLAUDE.md | 35 +++++++++++++++ web/chat.html | 38 ++++++++++------ web/zebra-audio.html | 17 ++++--- web/zebra-spaces.html | 102 +++++++++++++++++++++++++++--------------- 4 files changed, 137 insertions(+), 55 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1358b2f..5c830d6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -130,6 +130,41 @@ is not on the screen, peers cannot dox each other. Candidate types `rem.address` / `rem.port` are not. The WebRTC stack already obfuscates host candidates via mDNS by default — do not undo that work in the UI. +### CSS layout — grid only, no flex + +All page layout on every zebra page is **CSS grid**. No `display: flex` for +layout. Reasons we settled on this: + +- Grid lets us pin children to explicit columns (`grid-column: 1/2/3`) so + a `display:none` on one child never causes siblings to slide into its + slot. Flex auto-reorders, grid does not. +- One mental model for both axes. Flex needs rules per row + per item, + grid expresses the same intent in one `grid-template-*` block. +- A global `.hidden { display: none !important }` utility lives in the + page CSS — combined with explicit grid placement it produces a layout + that survives any child being toggled in or out. + +When refactoring or adding UI, use: + +- `display: grid` + `grid-template-columns` for column layout +- `grid-column: N` on every child of a grid so its position is explicit +- `grid-template-rows` + `grid-row` for vertical placement when needed +- `grid-template-areas` for small named-region layouts +- `gap` for spacing (instead of margins) + +Avoid: + +- `display: flex` on any container that arranges multiple elements + horizontally or vertically as part of the page layout +- Implicit positioning that relies on DOM order — always set + `grid-column` (and `grid-row` if relevant) on every grid child +- `flex-basis` / `flex-grow` mathematics — `1fr` is the equivalent and + reads cleaner + +If you need a one-off horizontal alignment of two short inline things +(e.g. a label + a value), grid still works fine +(`grid-template-columns: auto 1fr`). Don't reach for flex. + ### Web page integrity stamping Each deployed page (`web/chat.html`, `web/zebra-audio.html`, `web/how-it-works.html`, diff --git a/web/chat.html b/web/chat.html index dd3666e..1116e7c 100644 --- a/web/chat.html +++ b/web/chat.html @@ -53,11 +53,16 @@ button:disabled { opacity: 0.3; cursor: default; } button.invert { background: #000; color: #fff; } button.invert:hover:not(:disabled) { background: #333; } - .row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; flex-wrap: wrap; } + .row { + display: grid; grid-auto-flow: column; + grid-template-columns: max-content minmax(0, 1fr); + grid-auto-columns: max-content; + align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; + } .dot { width: 9px; height: 9px; border-radius: 50%; border: 1px solid #000; background: #fff; - flex-shrink: 0; transition: background 0.15s; + transition: background 0.15s; } .dot.on { background: #000; } .dot.warn { background: #888; } @@ -71,8 +76,11 @@ } input[type=text], input[type=password], select { width: 100%; } textarea { width: 100%; resize: vertical; min-height: 4.5rem; } - .field-row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; } - .field-row > input, .field-row > textarea { flex: 1; } + .field-row { + display: grid; grid-template-columns: minmax(0, 1fr) max-content; + gap: 0.5rem; margin-bottom: 0.5rem; + } + .field-row > input, .field-row > textarea { width: 100%; min-width: 0; } .key-display { border: 1px solid #ccc; padding: 0.5rem; font-family: monospace; font-size: 0.7rem; @@ -127,7 +135,8 @@ } details[open] > summary { margin-bottom: 0.5rem; } .mode-toggle { - display: inline-flex; border: 1px solid #000; + display: inline-grid; grid-auto-flow: column; grid-auto-columns: max-content; + border: 1px solid #000; font-size: 0.78rem; user-select: none; } .mode-toggle > label { @@ -152,11 +161,14 @@ .share-box .qr-canvas { width: 320px; height: 320px; border: 1px solid #ccc; background: #fff; - display: flex; align-items: center; justify-content: center; + display: grid; place-items: center; font-size: 0.65rem; color: #999; } .share-box input { width: 100%; font-size: 0.7rem; } - .share-box .copy-row { display: flex; gap: 0.4rem; margin-top: 0.3rem; } + .share-box .copy-row { + display: grid; grid-template-columns: minmax(0, 1fr) max-content; + gap: 0.4rem; margin-top: 0.3rem; + } .share-box .copy-row button { font-size: 0.75rem; padding: 0.25rem 0.7rem; } @media (max-width: 600px) { .share-box { grid-template-columns: 1fr; } @@ -250,7 +262,7 @@ try {

identity

- +
@@ -269,7 +281,7 @@ try {

room (passphrase mode)

+ style="width:100%; min-width:0">
@@ -307,12 +319,12 @@ try {
tx -
+
0%
rx -
+
@@ -1720,8 +1732,8 @@ logLine('sys', 'chat content lives in encrypted SRTP audio. no IP packets carry
page integrity  ·  built 2026-06-02
- md5 0fb2dca42673efdde9164f5bc24d0200
- sha256 ba3b4ce8b96e2649f76f9b0338bde38ddcdbea148adbdfb0e23094a85999ee2b
+ md5 5ae64bcf96a54da7ea55a0df88548920
+ sha256 06a3c011ec9670d85c6ab27b8decbfcc4f78e8a9be8186407670fdf4e4cc8dfd
hashes are of this page with these two fields zeroed — to verify, blank them and re-hash
one self-contained file — save a copy and verify it against these hashes; point it at your own servers with ?signal= and ?turncred=, or host your own community
diff --git a/web/zebra-audio.html b/web/zebra-audio.html index f2d64da..a057208 100644 --- a/web/zebra-audio.html +++ b/web/zebra-audio.html @@ -39,14 +39,19 @@ button:disabled { opacity: 0.3; cursor: default; } button.invert { background: #000; color: #fff; } button.invert:hover:not(:disabled) { background: #333; } - .row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; flex-wrap: wrap; } + .row { + display: grid; grid-auto-flow: column; + grid-template-columns: max-content minmax(0, 1fr); + grid-auto-columns: max-content; + align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; + } input[type=text] { font-family: monospace; font-size: 0.9rem; border: 1px solid #000; - padding: 0.45rem; background: #fff; color: #000; flex: 1; min-width: 0; + padding: 0.45rem; background: #fff; color: #000; min-width: 0; width: 100%; } select { font-family: monospace; font-size: 0.9rem; border: 1px solid #000; - padding: 0.45rem; background: #fff; color: #000; flex: 1; min-width: 0; cursor: pointer; + padding: 0.45rem; background: #fff; color: #000; min-width: 0; width: 100%; cursor: pointer; } .dot { width: 10px; height: 10px; border-radius: 50%; border: 1px solid #000; background: #fff; flex-shrink: 0; } .dot.ok { background: #060; border-color: #060; } @@ -703,7 +708,7 @@ else wirePuppet();