zebra-report: grid-only layout — fix controls-eats-viewport bug, no more flex

This commit is contained in:
russell@unturf.com 2026-06-02 09:12:48 -04:00
parent 03afea7dd6
commit 5d589d9986
No known key found for this signature in database
3 changed files with 102 additions and 55 deletions

View file

@ -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 {
<h2>identity</h2>
<div class="row">
<label for="handle-in">handle:</label>
<input type="text" id="handle-in" style="flex:1; min-width:0" placeholder="your handle">
<input type="text" id="handle-in" style="width:100%; min-width:0" placeholder="your handle">
<span class="status-line" id="id-status"></span>
</div>
<div class="row" style="margin-top:0.5rem">
@ -269,7 +281,7 @@ try {
<h2>room (passphrase mode)</h2>
<div class="row">
<input type="password" id="passphrase-in" placeholder="room passphrase — everyone in the room types the same one"
style="flex:1; min-width:0">
style="width:100%; min-width:0">
</div>
<div class="row">
<button id="btn-pp-join" class="invert">enter room</button>
@ -307,12 +319,12 @@ try {
</div>
<div class="row" style="margin-top:0.4rem">
<span class="note" style="min-width:3rem">tx</span>
<div class="meter" style="flex:1"><div class="meter-fill" id="meter-tx"></div></div>
<div class="meter" style="width:100%"><div class="meter-fill" id="meter-tx"></div></div>
<span class="note" id="gain-label" style="min-width:3rem; text-align:right">0%</span>
</div>
<div class="row">
<span class="note" style="min-width:3rem">rx</span>
<div class="meter" style="flex:1"><div class="meter-fill" id="meter-rx"></div></div>
<div class="meter" style="width:100%"><div class="meter-fill" id="meter-rx"></div></div>
<span class="note" id="rx-label" style="min-width:3rem; text-align:right"></span>
</div>
</section>
@ -1720,8 +1732,8 @@ logLine('sys', 'chat content lives in encrypted SRTP audio. no IP packets carry
</script>
<footer style="max-width:820px;margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span style="color:#777">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">0fb2dca42673efdde9164f5bc24d0200</span><br>
sha256 <span class="stamp-sha">ba3b4ce8b96e2649f76f9b0338bde38ddcdbea148adbdfb0e23094a85999ee2b</span><br>
md5 <span class="stamp-md5">5ae64bcf96a54da7ea55a0df88548920</span><br>
sha256 <span class="stamp-sha">06a3c011ec9670d85c6ab27b8decbfcc4f78e8a9be8186407670fdf4e4cc8dfd</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify it against these hashes; point it at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>

View file

@ -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();
<!-- secret: triple-click the footer "page integrity" seal -->
<!-- ============================================================ -->
<section id="puppet" hidden style="max-width:560px;margin:1.8rem auto 0;border:1px dashed #000;padding:0.9rem 1rem">
<h2 style="display:flex;align-items:center;justify-content:space-between">
<h2 style="display:grid;grid-template-columns:1fr auto;align-items:center;gap:0.5rem">
<span>puppet</span>
<button id="puppet-close" style="font-size:0.7rem;padding:0.15rem 0.6rem">close</button>
</h2>
@ -727,8 +732,8 @@ else wirePuppet();
<footer style="max-width:560px;margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">97b7b9f30c280b37917e4cd94befc4ba</span><br>
sha256 <span class="stamp-sha">1ed57fa7bca6be80d98ac418cccc30a6681b45341b36b6d263e9b090e7f3e8df</span><br>
md5 <span class="stamp-md5">47f8c9b6c4c4e3d2dc0544cf59da1efc</span><br>
sha256 <span class="stamp-sha">a9532ca9dbe91f11d63ece9073f558e3beef5e5ead524b85972c8f0436a8aa41</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify it against these hashes; point it at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>

View file

@ -101,23 +101,33 @@
* Both columns get min-width:0 so they shrink without forcing a
* horizontal scrollbar. */
/* three columns: cameras (left) | timeline/screen (middle) | controls (right).
* The 220px cameras track stays reserved even when no camera is live —
* .hidden hides the section content but the column itself stays put so
* the timeline (with the game iframe or a screen share) keeps its
* middle position instead of sliding leftmost. */
* Each child is pinned to its column with grid-column so any one of
* them going display:none can't cause the others to slide. Controls
* column is also clamped to ≤50vw so a very wide window doesn't let
* the side panel eat the screen-share area. */
.page {
display: grid;
grid-template-columns: 220px minmax(0, 1fr) minmax(0, 360px);
grid-template-columns: 220px minmax(0, 1fr) minmax(0, min(360px, 50vw));
gap: 1.25rem;
align-items: start;
}
.cameras-col { grid-column: 1; }
.timeline { grid-column: 2; }
.controls { grid-column: 3; }
.cameras-col {
min-width: 0;
border-right: 1px solid #ddd; padding-right: 1.25rem;
display: flex; flex-direction: column;
display: grid !important; /* beat the global .hidden util */
grid-auto-rows: max-content;
gap: 0.5rem;
}
/* the cameras-col uses the .hidden class as a content-empty signal —
* keep the column visible (track reserved) but hide its h2 + grid
* and drop the right-border/padding so the empty column reads as
* dead space rather than a divider for nothing */
.cameras-col.hidden h2,
.cameras-col.hidden #cameras { display: none; }
.cameras-col.hidden { border-right: none; padding-right: 0; }
/* hide-panel = focus mode: both side columns fold away, screen+timeline
* fills the full viewport. Cameras and controls both go to display:none
* so the screen-share / game iframe has nothing competing with it. */
@ -138,10 +148,14 @@
html.controls-collapsed-pref .page > .timeline { border-right: none; padding-right: 0; }
.timeline {
border-right: 1px solid #ddd; padding-right: 1.25rem; min-height: 60vh;
min-width: 0; display: flex; flex-direction: column; position: relative;
min-width: 0; display: grid; grid-auto-rows: max-content;
position: relative;
}
.controls { min-width: 0; }
.game-tabs { display: flex; gap: 0.4rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
.game-tabs {
display: grid; grid-auto-flow: column; grid-auto-columns: max-content;
gap: 0.4rem; margin-bottom: 0.5rem;
}
.game-tabs button {
background: #fff; color: #000; border: 1px solid #000;
padding: 0.3rem 0.8rem; font-family: monospace; font-size: 0.8rem; cursor: pointer;
@ -149,19 +163,20 @@
.game-tabs button.active { background: #000; color: #fff; }
.game-tabs button:hover:not(.active) { background: #f0f0f0; }
.timeline-frame {
flex: 1; min-height: 80vh; width: 100%;
min-height: 80vh; width: 100%;
border: 1px solid #ddd; background: #fff;
}
@media (max-width: 800px) {
body { padding: 1.25rem; }
/* stack on mobile: controls first, then any live screen, cameras last */
/* mobile: single column, all three sections stack to grid-column:1 */
.page { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
.controls { order: 1; }
.timeline { order: 2;
border-right: none; border-top: 1px solid #ddd;
padding-right: 0; padding-top: 1rem; min-height: 0; }
.cameras-col { order: 3;
width: 100%; border-right: none; border-top: 1px solid #ddd;
.controls, .timeline, .cameras-col { grid-column: 1; }
.controls { grid-row: 1; }
.timeline { grid-row: 2;
border-right: none; border-top: 1px solid #ddd;
padding-right: 0; padding-top: 1rem; min-height: 0; }
.cameras-col { grid-row: 3;
border-right: none; border-top: 1px solid #ddd;
padding-right: 0; padding-top: 1rem; }
#cameras { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
}
@ -176,14 +191,14 @@
#screens { display: grid; grid-template-columns: 1fr; gap: 0.75rem; }
.screen-tile {
border: 1px solid #000; background: #000; padding: 0;
display: flex; flex-direction: column; position: relative;
display: grid; grid-auto-rows: max-content; position: relative;
}
.screen-tile video {
width: 100%; height: auto; max-height: 92vh; display: block; background: #000;
}
.screen-tile .screen-meta {
background: #111; color: #ddd; font-size: 0.7rem; padding: 0.3rem 0.5rem;
display: flex; justify-content: space-between;
display: grid; grid-template-columns: 1fr auto; gap: 0.5rem; align-items: center;
}
/* Firefox Android rejects MediaStream <video> autoplay even when muted.
* When play() rejects we surface a tap-to-play overlay over the video
@ -191,11 +206,11 @@
* gesture so the retry play() succeeds. */
.screen-tile .tap-play {
position: absolute; left: 0; right: 0; top: 0; bottom: 1.6rem;
display: none; align-items: center; justify-content: center;
display: none; place-items: center;
background: rgba(0,0,0,0.65); color: #fff; cursor: pointer;
font-family: monospace; font-size: 1rem; user-select: none;
}
.screen-tile.needs-tap .tap-play { display: flex; }
.screen-tile.needs-tap .tap-play { display: grid; }
/* sec-screens visible → push everything else in the timeline column down or hide */
.timeline:has(> #sec-screens:not(.hidden)) > .game-tabs,
.timeline:has(> #sec-screens:not(.hidden)) > #game-frame { display: none; }
@ -205,7 +220,7 @@
#cameras { display: grid; grid-template-columns: 1fr; gap: 0.45rem; }
.camera-tile {
border: 1px solid #000; background: #000; padding: 0;
display: flex; flex-direction: column; position: relative;
display: grid; grid-auto-rows: max-content; position: relative;
}
.camera-tile video {
width: 100%; height: auto; display: block; background: #000;
@ -213,16 +228,16 @@
}
.camera-tile .screen-meta {
background: #111; color: #ddd; font-size: 0.7rem; padding: 0.2rem 0.4rem;
display: flex; justify-content: space-between;
display: grid; grid-template-columns: 1fr auto; gap: 0.4rem; align-items: center;
}
/* tap-to-play overlay applies to camera tiles too */
.camera-tile .tap-play {
position: absolute; left: 0; right: 0; top: 0; bottom: 1.2rem;
display: none; align-items: center; justify-content: center;
display: none; place-items: center;
background: rgba(0,0,0,0.65); color: #fff; cursor: pointer;
font-family: monospace; font-size: 0.9rem; user-select: none;
}
.camera-tile.needs-tap .tap-play { display: flex; }
.camera-tile.needs-tap .tap-play { display: grid; }
/* latency panel: one row per live PC, fixed-width columns so the numbers
* line up vertically as values bounce. lat-good < 50ms, mid < 150ms,
@ -267,10 +282,22 @@
button.invert { background: #000; color: #fff; }
button.invert:hover:not(:disabled) { background: #333; }
button.small { padding: 0.2rem 0.55rem; font-size: 0.75rem; }
.row { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.6rem; flex-wrap: wrap; }
/* form rows: inline strip with a single grow track for the input/select.
* grid-template-columns is generous enough for the common shapes:
* label input (1) label-stretchy
* label input button (2) label-stretchy-button
* button button span (3) buttons-stretchy
* Items past the third just flow into the row via grid-auto-flow. */
.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.6rem; margin-bottom: 0.6rem;
}
input[type=text], input[type=password] {
font-family: monospace; font-size: 0.9rem; border: 1px solid #000;
padding: 0.4rem; background: #fff; color: #000; flex: 1; min-width: 0;
padding: 0.4rem; background: #fff; color: #000; min-width: 0; width: 100%;
}
textarea {
font-family: monospace; font-size: 0.75rem; border: 1px solid #000;
@ -279,9 +306,9 @@
}
select {
font-family: monospace; font-size: 0.9rem; border: 1px solid #000;
padding: 0.4rem; background: #fff; color: #000; flex: 1; min-width: 0; cursor: pointer;
padding: 0.4rem; 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 { width: 10px; height: 10px; border-radius: 50%; border: 1px solid #000; background: #fff; }
.dot.ok { background: #060; border-color: #060; }
.dot.warn { background: #888; }
.status-line { font-size: 0.8rem; color: #555; }
@ -314,9 +341,10 @@
.mic { width: 18px; height: 18px; }
.mic svg { width: 18px; height: 18px; display: block; }
.mod-actions {
display: flex; gap: 0.3rem; flex-wrap: wrap;
display: grid; grid-auto-flow: column; grid-auto-columns: max-content;
gap: 0.3rem;
grid-column: 1 / -1; /* second row, full width */
justify-content: flex-start;
justify-content: start;
padding-left: 6.05rem; /* line up under the handle (badge col + gap) */
margin-top: 0.2rem;
}
@ -327,13 +355,15 @@
.log-line.err { color: #b00; }
.invite-banner {
border: 2px solid #000; padding: 0.8rem; margin-bottom: 1rem; background: #ffd;
display: flex; flex-direction: column; align-items: flex-start; gap: 0.6rem;
display: grid; grid-auto-rows: max-content; gap: 0.6rem; justify-items: start;
}
.invite-banner .invite-actions {
display: grid; grid-auto-flow: column; grid-auto-columns: max-content; gap: 0.5rem;
}
.invite-banner .invite-actions { display: flex; gap: 0.5rem; }
.notice-banner {
padding: 0.7rem 0.8rem; margin-bottom: 1rem;
display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
font-size: 0.85rem;
display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center;
gap: 0.6rem; font-size: 0.85rem;
}
.notice-banner.warn { border: 2px solid #b00; background: #fee; color: #b00; }
.notice-banner.info { border: 2px solid #060; background: #efe; color: #060; }
@ -2255,8 +2285,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">a6c36b06668308278c7645054fb5e432</span><br>
sha256 <span class="stamp-sha">cc3fe5cbc7e83183066c4ea09d1bb9d261a1a75c1abe51edfd6f2898e44d5374</span><br>
md5 <span class="stamp-md5">c58b4a17d9536b098d707ff650fbb9d3</span><br>
sha256 <span class="stamp-sha">4a105138a9b6e25c913dbbf9217a94a65912eee678caaebb28a07e96877bf35f</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>