SharpMeow commited on
Commit
3a4e505
·
verified ·
1 Parent(s): 7283d22

One page scroll bar and American English

Browse files
Files changed (5) hide show
  1. app.js +30 -10
  2. app.py +2 -2
  3. index.html +68 -3
  4. onescroll.html +62 -0
  5. style.css +30 -7
app.js CHANGED
@@ -175,21 +175,23 @@ function kitCommand(row) {
175
  function setCard(r) {
176
  $('c-kicker').textContent = `${r.scroll}${r.volume ? ' · VOLUME ' + r.volume : ''}${r.voxel_um ? ' · ' + r.voxel_um + ' µm' : ''}`;
177
  $('c-title').textContent = r.segment + (r.kind !== 'mesh' ? ' (' + r.mesh + ')' : '');
178
- const items = r.error ? [['not measured', r.error]] : [
 
 
 
179
  ['area', r.area_cm2 == null ? '-' : fmt(r.area_cm2) + ' cm²'],
180
  ['grid', `${r.grid_w} x ${r.grid_h}`],
181
  ['filled', pct(r.filled_share)],
182
- ['edges within 10% of the step', pct(r.within_10pct)],
183
  ['median edge / step', fmt(r.edge_p50, 3)],
184
- ['grid angle (median)', fmt(r.angle_p50, 1) + '°'],
185
- ['holes', `${fmt(r.holes_mm2)} mm² (${(r.holes_nodes || 0).toLocaleString()} nodes)`],
186
- ['folds', `${fmt(r.folds_mm2)} mm² (${(r.folds_nodes || 0).toLocaleString()} nodes)`],
187
- ['stretched', `${fmt(r.stretched_mm2)} mm² (${(r.stretched_nodes || 0).toLocaleString()} nodes)`],
188
  ['area vs meta.json', r.meta_area_ratio == null ? '-' : fmt(r.meta_area_ratio, 4)],
189
- ];
190
- $('c-nums').innerHTML = items.map(([k, v]) => `<dt>${k}</dt><dd>${esc(String(v))}</dd>`).join('');
 
 
191
  $('a-bucket').href = r.url + '/meta.json';
192
- $('a-bucket').textContent = 'meta.json on the bucket';
193
  }
194
 
195
  function esc(s) {
@@ -256,7 +258,24 @@ $('b-copy').addEventListener('click', async () => {
256
  try { await navigator.clipboard.writeText(kitCommand(current)); toast('kit command copied'); }
257
  catch { toast(kitCommand(current)); }
258
  });
259
- $('b-about').addEventListener('click', () => $('about').showModal());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
  // ---------------------------------------------------------------- table
262
 
@@ -296,6 +315,7 @@ $('tbl').addEventListener('click', (e) => {
296
  function setMode(m) {
297
  document.querySelectorAll('.chip[data-mode]').forEach((b) => b.classList.toggle('on', b.dataset.mode === m));
298
  $('table-view').hidden = m !== 'table';
 
299
  if (m === 'table') fillTable();
300
  }
301
  document.querySelectorAll('.chip[data-mode]').forEach((b) => b.addEventListener('click', () => setMode(b.dataset.mode)));
 
175
  function setCard(r) {
176
  $('c-kicker').textContent = `${r.scroll}${r.volume ? ' · VOLUME ' + r.volume : ''}${r.voxel_um ? ' · ' + r.voxel_um + ' µm' : ''}`;
177
  $('c-title').textContent = r.segment + (r.kind !== 'mesh' ? ' (' + r.mesh + ')' : '');
178
+ // one line per number, so the card fits beside the list without a scroll bar of its own
179
+ const dd = (v, cls = '') => `<dd${cls ? ` class="${cls}"` : ''}>${esc(String(v))}</dd>`;
180
+ const flag = (k, mm2, nodes) => `<dt>${k}</dt>${dd(fmt(mm2), 'v')}${dd((nodes || 0).toLocaleString(), 'u')}`;
181
+ $('c-nums').innerHTML = r.error ? `<dt>not measured</dt>${dd(r.error, 'wrap')}` : [
182
  ['area', r.area_cm2 == null ? '-' : fmt(r.area_cm2) + ' cm²'],
183
  ['grid', `${r.grid_w} x ${r.grid_h}`],
184
  ['filled', pct(r.filled_share)],
185
+ ['edges within 10%', pct(r.within_10pct)],
186
  ['median edge / step', fmt(r.edge_p50, 3)],
187
+ ['median grid angle', fmt(r.angle_p50, 1) + '°'],
 
 
 
188
  ['area vs meta.json', r.meta_area_ratio == null ? '-' : fmt(r.meta_area_ratio, 4)],
189
+ ].map(([k, v]) => `<dt>${k}</dt>${dd(v)}`).join('') +
190
+ '<dt class="hd">flagged</dt><dd class="v hd">mm²</dd><dd class="u hd">nodes</dd>' +
191
+ flag('holes', r.holes_mm2, r.holes_nodes) + flag('folds', r.folds_mm2, r.folds_nodes) +
192
+ flag('stretched', r.stretched_mm2, r.stretched_nodes);
193
  $('a-bucket').href = r.url + '/meta.json';
194
+ $('a-bucket').textContent = 'open meta.json';
195
  }
196
 
197
  function esc(s) {
 
258
  try { await navigator.clipboard.writeText(kitCommand(current)); toast('kit command copied'); }
259
  catch { toast(kitCommand(current)); }
260
  });
261
+ $('b-about').addEventListener('click', () => { $('about').showModal(); cover(); });
262
+ $('about').addEventListener('close', cover);
263
+
264
+ // One scroll bar at a time (one-screen viewer): the segment list scrolls, unless the table or the guide is open,
265
+ // which then scrolls instead. The panels under the table step aside; under the guide the list stops scrolling, and
266
+ // so does the table when the guide is opened over it.
267
+ function cover() {
268
+ const table = !$('table-view').hidden, guide = $('about').open;
269
+ $('picker').hidden = $('card').hidden = table;
270
+ document.documentElement.classList.toggle('covered', table || guide);
271
+ document.documentElement.classList.toggle('guide', guide);
272
+ }
273
+ // On a phone the picker sits above the card and takes the height the card leaves (style.css, --card-h).
274
+ if (window.ResizeObserver) {
275
+ new ResizeObserver(() => document.documentElement.style.setProperty('--card-h', $('card').offsetHeight + 'px'))
276
+ .observe($('card'));
277
+ }
278
+ if (window.matchMedia && matchMedia('(max-width: 760px), (max-height: 560px)').matches) $('c-more').open = false;
279
 
280
  // ---------------------------------------------------------------- table
281
 
 
315
  function setMode(m) {
316
  document.querySelectorAll('.chip[data-mode]').forEach((b) => b.classList.toggle('on', b.dataset.mode === m));
317
  $('table-view').hidden = m !== 'table';
318
+ cover();
319
  if (m === 'table') fillTable();
320
  }
321
  document.querySelectorAll('.chip[data-mode]').forEach((b) => b.addEventListener('click', () => setMode(b.dataset.mode)));
app.py CHANGED
@@ -33,7 +33,7 @@ LENS_CACHE = 32
33
  SORTS = ("area_cm2", "filled_share", "within_10pct", "holes_mm2", "folds_mm2", "stretched_mm2", "grid_h", "segment")
34
  # the page's colors (app.js COLORS), as 0-255 RGB: plain, hole, fold, stretched
35
  COLORS = {"base": (204, 214, 219), "hole": (89, 166, 255), "fold": (255, 92, 92), "str": (255, 166, 51)}
36
- WEB_FILES = ("index.html", "app.js", "style.css", "connect.html", "connect.json", "CONNECT.md")
37
  SUMMARY_KEYS = ("id", "scroll", "segment", "mesh", "kind", "volume", "voxel_um", "area_cm2", "filled_share",
38
  "within_10pct", "holes_mm2", "folds_mm2", "stretched_mm2", "clean", "prize_lists")
39
 
@@ -288,7 +288,7 @@ def get_segment(segment_id: str):
288
 
289
  def segment_picture(segment_id: str, view: str = "3d", azimuth: float = 30.0, elevation: float = 20.0):
290
  """A picture of one mesh's preview with its flags painted on, as on the page: holes blue, folds red,
291
- stretched cells orange, plain grey. Geometry only: no CT, no ink, and a clean surface is not proof the trace
292
  stays on one sheet. Only meshes with a preview (scrolls on no prize list) can be drawn.
293
 
294
  Args:
 
33
  SORTS = ("area_cm2", "filled_share", "within_10pct", "holes_mm2", "folds_mm2", "stretched_mm2", "grid_h", "segment")
34
  # the page's colors (app.js COLORS), as 0-255 RGB: plain, hole, fold, stretched
35
  COLORS = {"base": (204, 214, 219), "hole": (89, 166, 255), "fold": (255, 92, 92), "str": (255, 166, 51)}
36
+ WEB_FILES = ("index.html", "app.js", "style.css", "connect.html", "connect.json", "CONNECT.md", "onescroll.html")
37
  SUMMARY_KEYS = ("id", "scroll", "segment", "mesh", "kind", "volume", "voxel_um", "area_cm2", "filled_share",
38
  "within_10pct", "holes_mm2", "folds_mm2", "stretched_mm2", "clean", "prize_lists")
39
 
 
288
 
289
  def segment_picture(segment_id: str, view: str = "3d", azimuth: float = 30.0, elevation: float = 20.0):
290
  """A picture of one mesh's preview with its flags painted on, as on the page: holes blue, folds red,
291
+ stretched cells orange, plain gray. Geometry only: no CT, no ink, and a clean surface is not proof the trace
292
  stays on one sheet. Only meshes with a preview (scrolls on no prize list) can be drawn.
293
 
294
  Args:
index.html CHANGED
@@ -11,7 +11,69 @@
11
  <link rel="preconnect" href="https://fonts.googleapis.com">
12
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
  <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
14
- <link rel="stylesheet" href="style.css?v=1">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  <script type="importmap">
16
  { "imports": {
17
  "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
@@ -47,7 +109,10 @@
47
  <aside class="hud br panel" id="card" aria-live="polite">
48
  <div class="mono small dim" id="c-kicker">SELECT A SEGMENT</div>
49
  <h2 id="c-title">…</h2>
50
- <dl id="c-nums" class="nums mono small"></dl>
 
 
 
51
  <div class="legend mono small">
52
  <label><input type="checkbox" id="t-hole" checked> <span class="sw hole"></span>holes</label>
53
  <label><input type="checkbox" id="t-fold" checked> <span class="sw fold"></span>folds</label>
@@ -174,6 +239,6 @@ python -m kit tifxyz check MESH_URL --png check.png</pre>
174
  </dialog>
175
 
176
  <div id="toast" class="toast mono small" role="status" hidden></div>
177
- <script type="module" src="app.js?v=1"></script>
178
  </body>
179
  </html>
 
11
  <link rel="preconnect" href="https://fonts.googleapis.com">
12
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
  <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
14
+ <link rel="stylesheet" href="style.css?v=2">
15
+ <!-- space-onescroll:start -->
16
+ <!-- one page scroll bar: written by python -m kit spaceconnect render; do not edit -->
17
+ <style>iframe[srcdoc]{display:block;max-height:none!important}</style>
18
+ <script>
19
+ (function () {
20
+ if (window.__oneScroll) return;
21
+ window.__oneScroll = true;
22
+ var CSS = 'html,body{height:auto!important;min-height:0!important}html{overflow:hidden!important}';
23
+ function size(f) {
24
+ var d, st = f.__os;
25
+ try { d = f.contentDocument; } catch (e) { return; }
26
+ if (!d || !d.body || st.off) return;
27
+ var de = d.documentElement, r = Math.ceil(de.getBoundingClientRect().height), over = de.scrollHeight;
28
+ var ch = f.clientHeight, t = r;
29
+ if (over > ch + 1) t = Math.max(r, over);
30
+ else if (r >= st.r) t = Math.max(r, ch);
31
+ st.r = r;
32
+ if (Math.abs(t - ch) < 2) return;
33
+ var now = Date.now();
34
+ st.n = now - st.t0 < 4000 ? st.n + 1 : 1;
35
+ if (st.n === 1) st.t0 = now;
36
+ if (st.n > 40) { st.off = true; f.removeAttribute('scrolling'); var g = d.getElementById('space-onescroll'); if (g) g.remove(); return; }
37
+ f.style.setProperty('height', t + 'px', 'important');
38
+ }
39
+ function attach(f) {
40
+ var d, st = f.__os;
41
+ try { d = f.contentDocument; } catch (e) { return; }
42
+ if (!d || !d.documentElement || st.doc === d) return;
43
+ st.doc = d; st.r = Infinity; st.n = 0; st.off = false;
44
+ if (!d.getElementById('space-onescroll')) {
45
+ var s = d.createElement('style'); s.id = 'space-onescroll'; s.textContent = CSS;
46
+ (d.head || d.documentElement).appendChild(s);
47
+ }
48
+ f.setAttribute('scrolling', 'no');
49
+ f.style.setProperty('min-height', '0', 'important');
50
+ f.style.setProperty('max-height', 'none', 'important');
51
+ var w = d.defaultView, go = function () { size(f); };
52
+ if (w && w.ResizeObserver) { var ro = new w.ResizeObserver(go); ro.observe(d.documentElement); if (d.body) ro.observe(d.body); }
53
+ d.addEventListener('load', go, true);
54
+ go();
55
+ }
56
+ function bind(f) {
57
+ if (f.__os || f.getAttribute('data-scroll') === 'inner') return;
58
+ try { if (!f.contentDocument) return; } catch (e) { return; }
59
+ f.__os = {t0: 0, n: 0, r: 0};
60
+ f.addEventListener('load', function () { attach(f); });
61
+ if (f.contentDocument.readyState !== 'loading') attach(f);
62
+ else f.contentDocument.addEventListener('DOMContentLoaded', function () { attach(f); });
63
+ }
64
+ function scan() { var fs = document.getElementsByTagName('iframe'); for (var i = 0; i < fs.length; i++) bind(fs[i]); }
65
+ function start() {
66
+ scan();
67
+ new MutationObserver(scan).observe(document.documentElement, {childList: true, subtree: true});
68
+ window.addEventListener('resize', function () {
69
+ var fs = document.getElementsByTagName('iframe');
70
+ for (var i = 0; i < fs.length; i++) if (fs[i].__os) size(fs[i]);
71
+ });
72
+ }
73
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
74
+ })();
75
+ </script>
76
+ <!-- space-onescroll:end -->
77
  <script type="importmap">
78
  { "imports": {
79
  "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
 
109
  <aside class="hud br panel" id="card" aria-live="polite">
110
  <div class="mono small dim" id="c-kicker">SELECT A SEGMENT</div>
111
  <h2 id="c-title">…</h2>
112
+ <details id="c-more" class="more" open>
113
+ <summary class="mono dim">NUMBERS</summary>
114
+ <dl id="c-nums" class="nums mono small"></dl>
115
+ </details>
116
  <div class="legend mono small">
117
  <label><input type="checkbox" id="t-hole" checked> <span class="sw hole"></span>holes</label>
118
  <label><input type="checkbox" id="t-fold" checked> <span class="sw fold"></span>folds</label>
 
239
  </dialog>
240
 
241
  <div id="toast" class="toast mono small" role="status" hidden></div>
242
+ <script type="module" src="app.js?v=2"></script>
243
  </body>
244
  </html>
onescroll.html ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- space-onescroll:start -->
2
+ <!-- one page scroll bar: written by python -m kit spaceconnect render; do not edit -->
3
+ <style>iframe[srcdoc]{display:block;max-height:none!important}</style>
4
+ <script>
5
+ (function () {
6
+ if (window.__oneScroll) return;
7
+ window.__oneScroll = true;
8
+ var CSS = 'html,body{height:auto!important;min-height:0!important}html{overflow:hidden!important}';
9
+ function size(f) {
10
+ var d, st = f.__os;
11
+ try { d = f.contentDocument; } catch (e) { return; }
12
+ if (!d || !d.body || st.off) return;
13
+ var de = d.documentElement, r = Math.ceil(de.getBoundingClientRect().height), over = de.scrollHeight;
14
+ var ch = f.clientHeight, t = r;
15
+ if (over > ch + 1) t = Math.max(r, over);
16
+ else if (r >= st.r) t = Math.max(r, ch);
17
+ st.r = r;
18
+ if (Math.abs(t - ch) < 2) return;
19
+ var now = Date.now();
20
+ st.n = now - st.t0 < 4000 ? st.n + 1 : 1;
21
+ if (st.n === 1) st.t0 = now;
22
+ if (st.n > 40) { st.off = true; f.removeAttribute('scrolling'); var g = d.getElementById('space-onescroll'); if (g) g.remove(); return; }
23
+ f.style.setProperty('height', t + 'px', 'important');
24
+ }
25
+ function attach(f) {
26
+ var d, st = f.__os;
27
+ try { d = f.contentDocument; } catch (e) { return; }
28
+ if (!d || !d.documentElement || st.doc === d) return;
29
+ st.doc = d; st.r = Infinity; st.n = 0; st.off = false;
30
+ if (!d.getElementById('space-onescroll')) {
31
+ var s = d.createElement('style'); s.id = 'space-onescroll'; s.textContent = CSS;
32
+ (d.head || d.documentElement).appendChild(s);
33
+ }
34
+ f.setAttribute('scrolling', 'no');
35
+ f.style.setProperty('min-height', '0', 'important');
36
+ f.style.setProperty('max-height', 'none', 'important');
37
+ var w = d.defaultView, go = function () { size(f); };
38
+ if (w && w.ResizeObserver) { var ro = new w.ResizeObserver(go); ro.observe(d.documentElement); if (d.body) ro.observe(d.body); }
39
+ d.addEventListener('load', go, true);
40
+ go();
41
+ }
42
+ function bind(f) {
43
+ if (f.__os || f.getAttribute('data-scroll') === 'inner') return;
44
+ try { if (!f.contentDocument) return; } catch (e) { return; }
45
+ f.__os = {t0: 0, n: 0, r: 0};
46
+ f.addEventListener('load', function () { attach(f); });
47
+ if (f.contentDocument.readyState !== 'loading') attach(f);
48
+ else f.contentDocument.addEventListener('DOMContentLoaded', function () { attach(f); });
49
+ }
50
+ function scan() { var fs = document.getElementsByTagName('iframe'); for (var i = 0; i < fs.length; i++) bind(fs[i]); }
51
+ function start() {
52
+ scan();
53
+ new MutationObserver(scan).observe(document.documentElement, {childList: true, subtree: true});
54
+ window.addEventListener('resize', function () {
55
+ var fs = document.getElementsByTagName('iframe');
56
+ for (var i = 0; i < fs.length; i++) if (fs[i].__os) size(fs[i]);
57
+ });
58
+ }
59
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
60
+ })();
61
+ </script>
62
+ <!-- space-onescroll:end -->
style.css CHANGED
@@ -30,7 +30,9 @@ a { color: var(--accent); }
30
  .tl a { pointer-events: auto; }
31
  .tr { z-index: 5; top: var(--gut); right: var(--gut); display: flex; gap: 8px; align-items: center; }
32
  .bl { left: var(--gut); bottom: var(--gut); width: 300px; max-height: 52vh; }
33
- .br { right: var(--gut); bottom: var(--gut); width: 320px; max-height: 70vh; overflow: auto; }
 
 
34
  .kicker { font-size: 10.5px; letter-spacing: .14em; }
35
  h1 { font-family: var(--serif); font-weight: 400; font-size: clamp(34px, 5vw, 54px); margin: .15em 0 .1em; line-height: 1; }
36
  h1 em, h2 em { color: var(--accent); }
@@ -52,10 +54,15 @@ select, input[type=search] { background: #0a1217; color: var(--fg); border: 1px
52
  .chip.on { background: var(--fg); color: var(--bg); }
53
  .seg { display: flex; gap: 4px; }
54
  h2 { font-family: var(--serif); font-weight: 400; font-size: 24px; margin: .2em 0 .4em; word-break: break-word; }
55
- .nums { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; margin: 0 0 10px; }
 
56
  .nums dt { color: var(--dim); }
57
- .nums dd { margin: 0; text-align: right; }
58
- .legend { display: flex; flex-wrap: wrap; gap: 6px 12px; margin: 8px 0; }
 
 
 
 
59
  .legend label { display: flex; align-items: center; gap: 5px; cursor: pointer; }
60
  .sw { display: inline-block; width: 10px; height: 10px; border-radius: 2px; }
61
  .sw.hole { background: var(--hole); } .sw.fold { background: var(--fold); } .sw.str { background: var(--str); }
@@ -74,11 +81,27 @@ tbody tr.go { cursor: pointer; }
74
  .sheet-dialog { background: #0a1217; color: var(--fg); border: 1px solid var(--line-strong); border-radius: 12px; max-width: min(680px, calc(100vw - 32px)); padding: 18px 22px; line-height: 1.55; font-size: 13px; }
75
  .sheet-dialog::backdrop { background: rgba(0, 0, 0, .6); }
76
  .sheet-dialog .x { float: right; background: none; color: var(--fg); border: 0; font-size: 22px; cursor: pointer; }
77
- .sheet-dialog pre { background: #05080b; padding: 10px; border-radius: 6px; overflow: auto; }
78
  .toast { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); z-index: 6; background: var(--fg); color: var(--bg); padding: 8px 14px; border-radius: 999px; }
79
  .loading { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 2; color: var(--dim); }
 
 
 
 
 
 
 
80
  @media (max-width: 760px) {
81
  .tl .lede { display: none; }
82
- .bl { width: calc(100vw - 2 * var(--gut)); max-height: 34vh; bottom: auto; top: 150px; }
83
- .br { width: calc(100vw - 2 * var(--gut)); max-height: 34vh; }
 
 
 
 
 
 
 
 
 
84
  }
 
30
  .tl a { pointer-events: auto; }
31
  .tr { z-index: 5; top: var(--gut); right: var(--gut); display: flex; gap: 8px; align-items: center; }
32
  .bl { left: var(--gut); bottom: var(--gut); width: 300px; max-height: 52vh; }
33
+ /* One-screen viewer: the segment list is the one box that scrolls. The card grows to fit and never scrolls; on a
34
+ window too short for it, its bottom is cut (NUMBERS folds it back), so its top stays on screen below the buttons. */
35
+ .br { right: var(--gut); bottom: var(--gut); width: 330px; max-height: calc(100vh - 2 * var(--gut) - 44px); overflow: hidden; overflow: clip; }
36
  .kicker { font-size: 10.5px; letter-spacing: .14em; }
37
  h1 { font-family: var(--serif); font-weight: 400; font-size: clamp(34px, 5vw, 54px); margin: .15em 0 .1em; line-height: 1; }
38
  h1 em, h2 em { color: var(--accent); }
 
54
  .chip.on { background: var(--fg); color: var(--bg); }
55
  .seg { display: flex; gap: 4px; }
56
  h2 { font-family: var(--serif); font-weight: 400; font-size: 24px; margin: .2em 0 .4em; word-break: break-word; }
57
+ .more > summary { cursor: pointer; font-size: 10.5px; letter-spacing: .14em; margin: 0 0 6px; }
58
+ .nums { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 3px 10px; margin: 0 0 8px; }
59
  .nums dt { color: var(--dim); }
60
+ .nums dd { margin: 0; text-align: right; white-space: nowrap; grid-column: span 2; }
61
+ .nums dd.v, .nums dd.u { grid-column: auto; }
62
+ .nums dd.u { color: var(--dim); }
63
+ .nums dd.wrap { white-space: normal; }
64
+ .nums .hd { color: var(--dim); font-size: 10.5px; padding-top: 4px; }
65
+ .legend { display: flex; flex-wrap: wrap; gap: 6px 10px; margin: 8px 0; }
66
  .legend label { display: flex; align-items: center; gap: 5px; cursor: pointer; }
67
  .sw { display: inline-block; width: 10px; height: 10px; border-radius: 2px; }
68
  .sw.hole { background: var(--hole); } .sw.fold { background: var(--fold); } .sw.str { background: var(--str); }
 
81
  .sheet-dialog { background: #0a1217; color: var(--fg); border: 1px solid var(--line-strong); border-radius: 12px; max-width: min(680px, calc(100vw - 32px)); padding: 18px 22px; line-height: 1.55; font-size: 13px; }
82
  .sheet-dialog::backdrop { background: rgba(0, 0, 0, .6); }
83
  .sheet-dialog .x { float: right; background: none; color: var(--fg); border: 0; font-size: 22px; cursor: pointer; }
84
+ .sheet-dialog pre { background: #05080b; padding: 10px; border-radius: 6px; overflow-x: auto; }
85
  .toast { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); z-index: 6; background: var(--fg); color: var(--bg); padding: 8px 14px; border-radius: 999px; }
86
  .loading { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 2; color: var(--dim); }
87
+ /* While the table or the guide is open it is the one box that scrolls: the panels under it step aside. */
88
+ html.covered .list { overflow: hidden; }
89
+ html.guide .tablewrap { overflow: hidden; }
90
+ @media (max-height: 640px) {
91
+ .br h2 { font-size: 19px; }
92
+ .nums { gap: 2px 10px; }
93
+ }
94
  @media (max-width: 760px) {
95
  .tl .lede { display: none; }
96
+ /* Stacked under the title: the picker, then the card. The card keeps its own height (app.js writes it to
97
+ --card-h) up to what leaves the picker 225px, room for one row of the list; the picker takes what is left. */
98
+ .bl { width: calc(100vw - 2 * var(--gut)); bottom: auto; top: calc(var(--gut) + 78px);
99
+ max-height: max(225px, min(40vh, calc(100vh - 78px - 3 * var(--gut) - var(--card-h, 34vh)))); }
100
+ .br { width: calc(100vw - 2 * var(--gut)); max-height: calc(100vh - 78px - 3 * var(--gut) - 225px); }
101
+ .list { min-height: 56px; }
102
+ }
103
+ @media (max-width: 760px) and (max-height: 500px) {
104
+ /* a phone on its side: the picker and the card side by side, as on a wide screen */
105
+ .bl { top: auto; bottom: var(--gut); width: calc(50vw - 1.5 * var(--gut)); max-height: calc(100vh - 78px - 3 * var(--gut)); }
106
+ .br { width: calc(50vw - 1.5 * var(--gut)); max-height: calc(100vh - 2 * var(--gut) - 44px); }
107
  }