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 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();