diff --git a/board/AGENTS.md b/board/AGENTS.md
index aa988ef..25a6357 100644
--- a/board/AGENTS.md
+++ b/board/AGENTS.md
@@ -75,6 +75,14 @@ name in its accent, the logo's tagline underneath. Below the hero the page
goes back to being a plain readable document, because these are lists people
scan on a phone before breakfast.
+*A phone before breakfast* is a size, not a mood: the sheet makes wrapping the
+page **default** (`overflow-wrap` on `body`, inherited), so a run URL or a
+dotted test id in markup the theme has never seen — an organ's own reasons
+list, details block or footer — cannot push the page sideways. A board that
+needs a column on one line (`white-space:nowrap`) still may; what it must not
+do is re-declare the wrap per component, which is how the family drifted into
+one page that scrolled and one that did not.
+
Colour is information, never decoration: the accent is organ identity; pills
are row facets, toned so that only the *exception* is coloured (`supervised`
is 9 of 10 prompts in the Mind, so it stays neutral — tinting it would paint
diff --git a/board/_theme.py b/board/_theme.py
index b922340..2ffa22f 100644
--- a/board/_theme.py
+++ b/board/_theme.py
@@ -299,9 +299,20 @@ def mark(key):
--bad:#f85149;--accent:%(ink_dark)s;--tint:%(ink_dark)s1f;
--edge:%(ink_dark)s47}}
*{box-sizing:border-box}
+/* Wrapping is the page DEFAULT, not a per-component opt-in. These boards are
+ read on phones, and every one of them prints run URLs, dotted test ids and
+ long file paths — a single unbreakable token in any element a renderer adds
+ itself (a reasons list, a footer, a details block) spills past the right
+ edge and gives the WHOLE page a horizontal scroll. `overflow-wrap` is
+ inherited, so setting it here covers markup this module has never seen.
+ The three max-width/overflow rules do the same job for the things that
+ cannot be wrapped: an image, a table, a code block. */
body{margin:0 auto;max-width:44rem;padding:0 1rem 4rem;background:var(--bg);
color:var(--fg);font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",
- Helvetica,Arial,sans-serif;-webkit-text-size-adjust:100%%}
+ Helvetica,Arial,sans-serif;-webkit-text-size-adjust:100%%;
+ overflow-wrap:anywhere}
+img,svg,table{max-width:100%%}
+pre{overflow-x:auto}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
.muted{color:var(--muted)}
@@ -353,7 +364,7 @@ def mark(key):
.task{display:flex;gap:.6rem;align-items:flex-start;padding:.45rem .35rem;
margin:0 -.35rem;border-bottom:1px solid var(--line);border-radius:7px}
.task:hover{background:var(--tint)}
-.task p{margin:.25rem 0 0;flex:1;overflow-wrap:anywhere}
+.task p{margin:.25rem 0 0;flex:1}
button.copy{flex:0 0 auto;width:2.6rem;height:2.6rem;font-size:1.1rem;
border:1px solid var(--line);border-radius:9px;background:var(--btn);
cursor:pointer;color:var(--fg);transition:border-color .12s,color .12s}
@@ -402,7 +413,7 @@ def mark(key):
/* --- tables ------------------------------------------------------------ */
table.recent{width:100%%;border-collapse:collapse;font-size:.95em}
table.recent td{border-bottom:1px solid var(--line);
- padding:.45rem .4rem .45rem 0;vertical-align:top;overflow-wrap:anywhere}
+ padding:.45rem .4rem .45rem 0;vertical-align:top}
table.recent tr:hover td{background:var(--tint)}
table.recent td.when{white-space:nowrap;color:var(--muted);
font-variant-numeric:tabular-nums}
diff --git a/tests/test_board_theme.py b/tests/test_board_theme.py
index 6276b9c..4406f48 100644
--- a/tests/test_board_theme.py
+++ b/tests/test_board_theme.py
@@ -174,3 +174,35 @@ def test_boards_footer_skips_self_and_tags_each_sibling():
def test_stats_render_pairs_and_vanish_when_empty():
assert _theme.stats() == ""
assert "3In flight" in _theme.stats((3, "In flight"))
+
+
+# --- the phone invariant: nothing may push the page sideways ---------------
+# These boards are read on a phone before breakfast. A single unbreakable
+# token — a run URL, a dotted test id, a workspace script path — used to
+# spill past the right edge and give the WHOLE page a horizontal scroll,
+# because wrapping was declared per component (`.task p`, `table.recent td`)
+# and every organ's own markup (a reasons list, a details block, a footer)
+# missed out. The guard belongs on `body`, where it is inherited by markup
+# this module has never seen.
+
+
+def test_wrapping_is_the_page_default_not_a_per_component_opt_in():
+ css = _theme.css("mind")
+ body = re.search(r"^body\{(.*?)\}", css, re.S | re.M).group(1)
+ assert "overflow-wrap:anywhere" in body.replace("\n ", "")
+
+
+def test_the_things_that_cannot_wrap_are_bounded_instead():
+ # An image, a table or a code block has no soft wrap opportunity to take;
+ # each is held inside the column or given its own scroller.
+ css = _theme.css("mind")
+ assert "img,svg,table{max-width:100%}" in css
+ assert "pre{overflow-x:auto}" in css
+
+
+def test_no_component_rule_re_declares_the_wrap():
+ """One place, not seven — a component that sets its own wrap is a rule
+ that will be forgotten by the next board that adds a list."""
+ css = re.sub(r"/\*.*?\*/", "", _theme.css("mind"), flags=re.S)
+ body_rule = re.search(r"^body\{.*?\}", css, re.S | re.M).group(0)
+ assert css.replace(body_rule, "").count("overflow-wrap") == 0