diff --git a/dashboard/src/gpsntp_dashboard/static/index.html b/dashboard/src/gpsntp_dashboard/static/index.html
index 46d1086..0d8ef86 100644
--- a/dashboard/src/gpsntp_dashboard/static/index.html
+++ b/dashboard/src/gpsntp_dashboard/static/index.html
@@ -174,7 +174,7 @@
-
diff --git a/dashboard/src/gpsntp_dashboard/static/style.css b/dashboard/src/gpsntp_dashboard/static/style.css
index a172bea..0e3071b 100644
--- a/dashboard/src/gpsntp_dashboard/static/style.css
+++ b/dashboard/src/gpsntp_dashboard/static/style.css
@@ -100,8 +100,22 @@ svg { width: 1.15em; height: 1.15em; fill: none; stroke: currentColor; stroke-wi
@keyframes pulse { 70% { box-shadow: 0 0 0 7px rgba(52, 211, 153, 0); } 100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); } }
/* ---------- layout ---------- */
-main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18px; }
+/* `grid-template-columns: minmax(0, 1fr)` is load-bearing, not decoration.
+ *
+ * A grid column defaults to `auto`, whose MINIMUM is min-content — and grid items
+ * refuse to shrink below it. The sources table has seven columns, giving it a
+ * ~700px min-content, and that number propagates up and pins `main` to 771px
+ * inside a 375px phone. Every panel then overflows and the document gets a
+ * horizontal scrollbar. `.table-hold { overflow-x: auto }` cannot rescue it,
+ * because the wrapper's own ancestors still won't shrink.
+ *
+ * The 0 in minmax(0, 1fr) overrides that automatic min-content floor. Same reason
+ * .panel needs min-width: 0. Remove either and mobile breaks again.
+ */
+main { max-width: 1180px; margin: 0 auto; padding: 20px;
+ display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px; }
.panel {
+ min-width: 0;
background: linear-gradient(var(--panel), var(--bg-2));
border: 1px solid var(--border-soft); border-radius: 16px; padding: 18px;
box-shadow: var(--shadow);
@@ -225,7 +239,7 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p
* Clockmakers signed the backplate — the brass face only a repairer sees once
* the case is open. This footer is that plate: a double hairline for the plate
* edge, engraved small-caps for the name, and nothing that moves. A signature
- * should be quiet. Same markup as the docs site at escapement.warehack.ing,
+ * should be quiet. Same markup as the docs site at cuckoo.warehack.ing,
* recolored from brass to this dashboard's cyan.
*/
.ss-plate__link {
@@ -254,10 +268,77 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p
.ss-plate__cta svg { width: 14px; height: 14px; transition: transform .2s; }
.ss-plate__link:hover .ss-plate__cta svg { transform: translateX(2px); }
+/* ---------- mobile ----------
+ *
+ * This is a status page. On a phone you are almost always asking ONE question:
+ * "is the clock still good?" So the hero answer — time, stratum, lock — must be
+ * readable without scrolling, and everything else can wait below the fold.
+ *
+ * The tables and the signal bars scroll sideways WITHIN their panels rather than
+ * being crushed into unreadability. That's deliberate: a squeezed IPv6 address
+ * wrapped over four lines is worse than one you swipe.
+ */
+@media (max-width: 700px) {
+ main { padding: 14px; gap: 14px; }
+ .panel { padding: 14px; border-radius: 14px; }
+
+ /* A panel head is a title plus its readout. Side by side they fight for a
+ narrow line and the title wraps mid-phrase; stacked, both stay whole. */
+ .panel-head { flex-direction: column; align-items: flex-start; gap: 6px; }
+ .spark-meta { font-size: 11px; }
+
+ /* Stack the clock above the stratum badge, and left-align the badge so the
+ eye travels straight down instead of ping-ponging left-right. */
+ .hero { flex-direction: column; align-items: flex-start; gap: 14px; }
+ .verdict { text-align: left; width: 100%; }
+ .ref-line { margin-top: 6px; }
+
+ /* clamp() already scales this, but 11vw on a narrow phone leaves the seconds
+ hard against the edge. Give it a slightly bigger floor and tighter tracking. */
+ .clock { font-size: clamp(38px, 13vw, 60px); letter-spacing: 0; }
+
+ /* Two columns, not one: six stat cards single-file is a lot of thumb. */
+ .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
+ .card { padding: 12px; }
+ .card-value { font-size: 21px; }
+
+ .spark-big { height: 90px; }
+ .snr { min-height: 150px; }
+
+ /* Swipe the source table rather than wrap it. The shadow hint tells the reader
+ there IS more to the right — otherwise a cut-off column just looks broken. */
+ .table-hold {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ background:
+ linear-gradient(to right, var(--panel) 30%, transparent) left / 24px 100% no-repeat,
+ linear-gradient(to left, var(--panel) 30%, transparent) right / 24px 100% no-repeat,
+ radial-gradient(farthest-side at 0 50%, rgba(0,0,0,.35), transparent) left / 12px 100% no-repeat,
+ radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.35), transparent) right / 12px 100% no-repeat;
+ background-attachment: local, local, scroll, scroll;
+ }
+ .tbl { min-width: 560px; }
+
+ .topbar { padding: 12px 14px; }
+ .brand h1 { font-size: 17px; }
+ .brand-sub { font-size: 11.5px; }
+ .brand-icon { width: 26px; height: 26px; }
+
+ .foot { padding: 10px 14px 24px; }
+ /* Three items on one line is unreadable at 375px — let them stack. */
+ .foot-meta { flex-direction: column; align-items: flex-start; gap: 6px; }
+}
+
@media (max-width: 560px) {
.ss-plate__link { flex-direction: column; align-items: flex-start; }
}
+/* Comfortable thumb targets. The WCAG 2.2 minimum is 24px; 44px is the iOS HIG
+ figure and the one that actually feels right when you're standing up. */
+@media (pointer: coarse) {
+ .foot-docs, .ss-plate__link { min-height: 44px; }
+}
+
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
/* "approx" marker when GPSNTP_POSITION=coarse */