openvibers/openvibe.shared Commits
4.47.1: no page is wider than a phone (#190)
Changes
@@@ -6,6 +6,15 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).## Unreleased++## 4.47.1 — 2026-10-30++++- **No page is wider than a phone.** A sideways row (`.sc-row`, `.ovk-tabs`, `.ovk-stats`) is `position: relative`, so a++ hidden label (`.sr-only`, `position: absolute`) inside a card past the edge stays in the row instead of widening the++ page. The phone hero's watermark (`.sc-hero-vis`) sits inside the hero's corner instead of 54 px past the screen;++ five homes were 434-451 px wide on a 390 px phone.++- docs/kit.md "Scrolling on touch screens": the rules for boxes, sheets and sideways rows on touch screens, and the++ width check.++## 4.47.0 — 2026-10-29- **The kit renders natively, on Android and iOS.** `scripts/native/compose/KitRenderer.kt` (`KitView(doc: KitNode)`, a@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.47.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.47.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.47.0",++ "version": "4.47.1","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -508,6 +508,24 @@ do most of that:Lists that are text (questions, chips, table rows) are cut on phones instead, with `nth-child` and a link to the wholelist; a closing call that repeats the hero, or a "find your way around" grid that repeats the nav, is left out.++### Scrolling on touch screens (4.47.1)++++A finger drag must always move something. The rules every site follows:++++- A short list flows in the page. It has no box of its own.++- A long or live list (chat, a transcript, chapters on a phone) keeps a box no taller than 60% of the screen++ (`max-height: min(60svh, 26rem)`) with `overscroll-behavior-y: auto`. At the box's end the drag carries on into++ the page. `contain` there traps the finger (openvibe.video's chapters, 10-29).++- Sheets, menus and dialogs keep `overscroll-behavior: contain` and lock the page behind them.++- A sideways row (`.sc-row`, `.ovk-tabs`, a site's own card strip) is `position: relative` as well as `overflow-x: auto`.++ Without that, a hidden label (`.sr-only`, `position: absolute`) inside a card past the edge is placed outside the++ row and widens the whole page: openvibe.coupons was 2208 px wide on a 390 px phone and search.openvibe.network++ 3407 px.++- Decoration (a hero's watermark, a turning ring) stays inside its section, or the section sets `overflow-x: clip`.++++Check a page with `document.documentElement.scrollWidth`, which must equal the screen width at 320, 360 and 390 px,++with and without touch.++## Balances`wallet: true` (or a site's own `coins` and `vibes` chips) shows the balances as one calm pill on phones. Each balance@@@ -83,7 +83,7 @@.ovk-sub { margin: 2px 0 0; color: var(--ovk-muted); font-size: .86rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ovk-head-tools { display: flex; align-items: center; gap: 2px; margin-right: -8px; }.ovk-head-tools form { margin: 0; }--.ovk-tabs { display: flex; gap: 4px; margin: 0 -16px; padding: 0 16px 8px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }++.ovk-tabs { position: relative; display: flex; gap: 4px; margin: 0 -16px; padding: 0 16px 8px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }.ovk-tabs::-webkit-scrollbar { display: none; }.ovk-tabs a { display: inline-flex; align-items: center; gap: 6px; flex: none; min-height: 36px; padding: 6px 14px; border-radius: 999px; color: var(--ovk-muted); font-size: .9rem; font-weight: 650; text-decoration: none; white-space: nowrap; }.ovk-tabs a:hover { color: var(--ovk-text); background: var(--ovk-panel-2); }@@@ -209,7 +209,7 @@ div.ovk-set-row { cursor: default; }}/* ── Numbers, empty states, notices, code, sheets ── */--.ovk-stats { display: flex; gap: 8px; margin: 12px -16px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; }++.ovk-stats { position: relative; display: flex; gap: 8px; margin: 12px -16px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; }.ovk-stats::-webkit-scrollbar { display: none; }.ovk-stats > div { display: flex; flex-direction: column-reverse; flex: 0 0 auto; min-width: 104px; box-sizing: border-box; padding: 8px 14px; border: 1px solid var(--ovk-line); border-radius: 12px; background: var(--ovk-panel); }.ovk-stats dt { margin: 0; color: var(--ovk-muted); font-size: .76rem; line-height: 1.25; }@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.47.0",++ "version": "4.47.1","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -29,7 +29,7 @@--ovk-bottom: var(--ovnav-appbar-h, 0px); /* the app bar's height on phones (navbar.js sets it) */--ovk-font: var(--ov-font, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);}--.ovk-stats { display: flex; gap: 8px; margin: 12px -16px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; }++.ovk-stats { position: relative; display: flex; gap: 8px; margin: 12px -16px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; }.ovk-stats::-webkit-scrollbar { display: none; }.ovk-stats > div { display: flex; flex-direction: column-reverse; flex: 0 0 auto; min-width: 104px; box-sizing: border-box; padding: 8px 14px; border: 1px solid var(--ovk-line); border-radius: 12px; background: var(--ovk-panel); }.ovk-stats dt { margin: 0; color: var(--ovk-muted); font-size: .76rem; line-height: 1.25; }@@@ -27,7 +27,7 @@ a.sc-orb-node:hover .sc-orb-face>*:first-child,a.sc-orb-node:focus-visible .sc-ohtml[data-ov-motion=reduced] .sc-ring,html[data-ov-motion=reduced] .sc-aura-glow,html[data-ov-motion=reduced] .sc-orb-ring,html[data-ov-motion=reduced] .sc-orb-face{animation:none}@media (max-width:860px){.sc-hero.sc-has-vis{display:block}--.sc-hero.sc-has-vis .sc-hero-vis{position:absolute;z-index:-1;top:-26px;right:-54px;width:180px;opacity:.36;pointer-events:none}++.sc-hero.sc-has-vis .sc-hero-vis{position:absolute;z-index:-1;top:-26px;right:0;width:180px;opacity:.36;pointer-events:none}.sc-hero.sc-has-vis .sc-aura .sc-vis-mark{width:46px!important;height:46px!important}.sc-hero.sc-has-orbit{display:flex;flex-direction:column}.sc-hero.sc-has-orbit .sc-hero-vis{order:-1;position:static;width:auto;opacity:1;pointer-events:auto;margin:0 0 6px}@@@ -149,7 +149,7 @@ a.sc-stat:hover{border-color:var(--accent,#3b82f6)}/* A list that is one sideways row on a phone (scroll snap, the next card peeking) and whatever the page made it on widescreens. Every item stays in the HTML. --sc-row-w sets a card's width (82% by default; 46% for small tiles). */@media (max-width:599px){--.sc-row.sc-row{display:flex;flex-wrap:nowrap;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin-left:-16px;margin-right:-16px;padding:0 16px 4px;grid-template-columns:none}++.sc-row.sc-row{position:relative;display:flex;flex-wrap:nowrap;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin-left:-16px;margin-right:-16px;padding:0 16px 4px;grid-template-columns:none}.sc-row.sc-row::-webkit-scrollbar{display:none}.sc-row.sc-row>*{flex:0 0 var(--sc-row-w,82%);min-width:0;scroll-snap-align:start}.sc-fold-hint{display:none}@@@ -447,7 +447,7 @@ a.sc-orb-node:hover .sc-orb-face>*:first-child,a.sc-orb-node:focus-visible .sc-ohtml[data-ov-motion=reduced] .sc-ring,html[data-ov-motion=reduced] .sc-aura-glow,html[data-ov-motion=reduced] .sc-orb-ring,html[data-ov-motion=reduced] .sc-orb-face{animation:none}@media (max-width:860px){.sc-hero.sc-has-vis{display:block}--.sc-hero.sc-has-vis .sc-hero-vis{position:absolute;z-index:-1;top:-26px;right:-54px;width:180px;opacity:.36;pointer-events:none}++.sc-hero.sc-has-vis .sc-hero-vis{position:absolute;z-index:-1;top:-26px;right:0;width:180px;opacity:.36;pointer-events:none}.sc-hero.sc-has-vis .sc-aura .sc-vis-mark{width:46px!important;height:46px!important}.sc-hero.sc-has-orbit{display:flex;flex-direction:column}.sc-hero.sc-has-orbit .sc-hero-vis{order:-1;position:static;width:auto;opacity:1;pointer-events:auto;margin:0 0 6px}@@@ -569,7 +569,7 @@ a.sc-stat:hover{border-color:var(--accent,#3b82f6)}/* A list that is one sideways row on a phone (scroll snap, the next card peeking) and whatever the page made it on widescreens. Every item stays in the HTML. --sc-row-w sets a card's width (82% by default; 46% for small tiles). */@media (max-width:599px){--.sc-row.sc-row{display:flex;flex-wrap:nowrap;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin-left:-16px;margin-right:-16px;padding:0 16px 4px;grid-template-columns:none}++.sc-row.sc-row{position:relative;display:flex;flex-wrap:nowrap;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin-left:-16px;margin-right:-16px;padding:0 16px 4px;grid-template-columns:none}.sc-row.sc-row::-webkit-scrollbar{display:none}.sc-row.sc-row>*{flex:0 0 var(--sc-row-w,82%);min-width:0;scroll-snap-align:start}.sc-fold-hint{display:none}@@@ -28,7 +28,8 @@ assert.ok(fo.includes('<div class="sc-fold-body"><section class="sc-sec"'), 'othassert.ok(sc.fold({ title: 'T', html: '<p>x</p>', open: true }).includes('<details class="sc-fold" open>'), 'open on request');assert.strictEqual(sc.fold({ title: 'T', html: '' }), '', 'nothing to fold renders nothing');assert.strictEqual(sc.fold({ html: '<p>x</p>' }), '', 'a fold needs a heading');--assert.ok(sc.CSS.includes('.sc-row.sc-row{display:flex;flex-wrap:nowrap'), 'the phone row does not inherit a wrap');++assert.ok(sc.CSS.includes('.sc-row.sc-row{position:relative;display:flex;flex-wrap:nowrap'), 'the phone row does not inherit a wrap, and holds what it clips (4.47.1)');++assert.ok(/\.sc-hero\.sc-has-vis \.sc-hero-vis\{[^}]*right:0[;}]/.test(sc.CSS), 'the phone hero watermark stays inside the screen (4.47.1)');// A set of six or nine apps is three a row; eight is four; seven keeps the grid's own columns.const appsN = (n) => sc.apps({ items: Array.from({ length: n }, (_, i) => ({ name: `App ${i}`, href: `/a${i}` })) });
@@ -6,6 +6,15 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -6,6 +6,15 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
## Unreleased
+## 4.47.1 — 2026-10-30
+
+- **No page is wider than a phone.** A sideways row (`.sc-row`, `.ovk-tabs`, `.ovk-stats`) is `position: relative`, so a
+ hidden label (`.sr-only`, `position: absolute`) inside a card past the edge stays in the row instead of widening the
+ page. The phone hero's watermark (`.sc-hero-vis`) sits inside the hero's corner instead of 54 px past the screen;
+ five homes were 434-451 px wide on a 390 px phone.
+- docs/kit.md "Scrolling on touch screens": the rules for boxes, sheets and sideways rows on touch screens, and the
+ width check.
+
## 4.47.0 — 2026-10-29
## 4.47.0 — 2026-10-29
- **The kit renders natively, on Android and iOS.** `scripts/native/compose/KitRenderer.kt` (`KitView(doc: KitNode)`, a
- **The kit renders natively, on Android and iOS.** `scripts/native/compose/KitRenderer.kt` (`KitView(doc: KitNode)`, a
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.47.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.47.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.47.0",
+ "version": "4.47.1",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -508,6 +508,24 @@ do most of that:
@@ -508,6 +508,24 @@ do most of that:
Lists that are text (questions, chips, table rows) are cut on phones instead, with `nth-child` and a link to the whole
Lists that are text (questions, chips, table rows) are cut on phones instead, with `nth-child` and a link to the whole
list; a closing call that repeats the hero, or a "find your way around" grid that repeats the nav, is left out.
list; a closing call that repeats the hero, or a "find your way around" grid that repeats the nav, is left out.
+### Scrolling on touch screens (4.47.1)
+
+A finger drag must always move something. The rules every site follows:
+
+- A short list flows in the page. It has no box of its own.
+- A long or live list (chat, a transcript, chapters on a phone) keeps a box no taller than 60% of the screen
+ (`max-height: min(60svh, 26rem)`) with `overscroll-behavior-y: auto`. At the box's end the drag carries on into
+ the page. `contain` there traps the finger (openvibe.video's chapters, 10-29).
+- Sheets, menus and dialogs keep `overscroll-behavior: contain` and lock the page behind them.
+- A sideways row (`.sc-row`, `.ovk-tabs`, a site's own card strip) is `position: relative` as well as `overflow-x: auto`.
+ Without that, a hidden label (`.sr-only`, `position: absolute`) inside a card past the edge is placed outside the
+ row and widens the whole page: openvibe.coupons was 2208 px wide on a 390 px phone and search.openvibe.network
+ 3407 px.
+- Decoration (a hero's watermark, a turning ring) stays inside its section, or the section sets `overflow-x: clip`.
+
+Check a page with `document.documentElement.scrollWidth`, which must equal the screen width at 320, 360 and 390 px,
+with and without touch.
+
## Balances
## Balances
`wallet: true` (or a site's own `coins` and `vibes` chips) shows the balances as one calm pill on phones. Each balance
`wallet: true` (or a site's own `coins` and `vibes` chips) shows the balances as one calm pill on phones. Each balance
@@ -83,7 +83,7 @@
@@ -83,7 +83,7 @@
.ovk-sub { margin: 2px 0 0; color: var(--ovk-muted); font-size: .86rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovk-sub { margin: 2px 0 0; color: var(--ovk-muted); font-size: .86rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovk-head-tools { display: flex; align-items: center; gap: 2px; margin-right: -8px; }
.ovk-head-tools { display: flex; align-items: center; gap: 2px; margin-right: -8px; }
.ovk-head-tools form { margin: 0; }
.ovk-head-tools form { margin: 0; }
-.ovk-tabs { display: flex; gap: 4px; margin: 0 -16px; padding: 0 16px 8px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
+.ovk-tabs { position: relative; display: flex; gap: 4px; margin: 0 -16px; padding: 0 16px 8px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.ovk-tabs::-webkit-scrollbar { display: none; }
.ovk-tabs::-webkit-scrollbar { display: none; }
.ovk-tabs a { display: inline-flex; align-items: center; gap: 6px; flex: none; min-height: 36px; padding: 6px 14px; border-radius: 999px; color: var(--ovk-muted); font-size: .9rem; font-weight: 650; text-decoration: none; white-space: nowrap; }
.ovk-tabs a { display: inline-flex; align-items: center; gap: 6px; flex: none; min-height: 36px; padding: 6px 14px; border-radius: 999px; color: var(--ovk-muted); font-size: .9rem; font-weight: 650; text-decoration: none; white-space: nowrap; }
.ovk-tabs a:hover { color: var(--ovk-text); background: var(--ovk-panel-2); }
.ovk-tabs a:hover { color: var(--ovk-text); background: var(--ovk-panel-2); }
@@ -209,7 +209,7 @@ div.ovk-set-row { cursor: default; }
@@ -209,7 +209,7 @@ div.ovk-set-row { cursor: default; }
}
}
/* ── Numbers, empty states, notices, code, sheets ── */
/* ── Numbers, empty states, notices, code, sheets ── */
-.ovk-stats { display: flex; gap: 8px; margin: 12px -16px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; }
+.ovk-stats { position: relative; display: flex; gap: 8px; margin: 12px -16px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; }
.ovk-stats::-webkit-scrollbar { display: none; }
.ovk-stats::-webkit-scrollbar { display: none; }
.ovk-stats > div { display: flex; flex-direction: column-reverse; flex: 0 0 auto; min-width: 104px; box-sizing: border-box; padding: 8px 14px; border: 1px solid var(--ovk-line); border-radius: 12px; background: var(--ovk-panel); }
.ovk-stats > div { display: flex; flex-direction: column-reverse; flex: 0 0 auto; min-width: 104px; box-sizing: border-box; padding: 8px 14px; border: 1px solid var(--ovk-line); border-radius: 12px; background: var(--ovk-panel); }
.ovk-stats dt { margin: 0; color: var(--ovk-muted); font-size: .76rem; line-height: 1.25; }
.ovk-stats dt { margin: 0; color: var(--ovk-muted); font-size: .76rem; line-height: 1.25; }
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.47.0",
+ "version": "4.47.1",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -29,7 +29,7 @@
@@ -29,7 +29,7 @@
--ovk-bottom: var(--ovnav-appbar-h, 0px); /* the app bar's height on phones (navbar.js sets it) */
--ovk-bottom: var(--ovnav-appbar-h, 0px); /* the app bar's height on phones (navbar.js sets it) */
--ovk-font: var(--ov-font, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
--ovk-font: var(--ov-font, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
}
}
-.ovk-stats { display: flex; gap: 8px; margin: 12px -16px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; }
+.ovk-stats { position: relative; display: flex; gap: 8px; margin: 12px -16px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; }
.ovk-stats::-webkit-scrollbar { display: none; }
.ovk-stats::-webkit-scrollbar { display: none; }
.ovk-stats > div { display: flex; flex-direction: column-reverse; flex: 0 0 auto; min-width: 104px; box-sizing: border-box; padding: 8px 14px; border: 1px solid var(--ovk-line); border-radius: 12px; background: var(--ovk-panel); }
.ovk-stats > div { display: flex; flex-direction: column-reverse; flex: 0 0 auto; min-width: 104px; box-sizing: border-box; padding: 8px 14px; border: 1px solid var(--ovk-line); border-radius: 12px; background: var(--ovk-panel); }
.ovk-stats dt { margin: 0; color: var(--ovk-muted); font-size: .76rem; line-height: 1.25; }
.ovk-stats dt { margin: 0; color: var(--ovk-muted); font-size: .76rem; line-height: 1.25; }
@@ -27,7 +27,7 @@ a.sc-orb-node:hover .sc-orb-face>*:first-child,a.sc-orb-node:focus-visible .sc-o
@@ -27,7 +27,7 @@ a.sc-orb-node:hover .sc-orb-face>*:first-child,a.sc-orb-node:focus-visible .sc-o
html[data-ov-motion=reduced] .sc-ring,html[data-ov-motion=reduced] .sc-aura-glow,html[data-ov-motion=reduced] .sc-orb-ring,html[data-ov-motion=reduced] .sc-orb-face{animation:none}
html[data-ov-motion=reduced] .sc-ring,html[data-ov-motion=reduced] .sc-aura-glow,html[data-ov-motion=reduced] .sc-orb-ring,html[data-ov-motion=reduced] .sc-orb-face{animation:none}
@media (max-width:860px){
@media (max-width:860px){
.sc-hero.sc-has-vis{display:block}
.sc-hero.sc-has-vis{display:block}
-.sc-hero.sc-has-vis .sc-hero-vis{position:absolute;z-index:-1;top:-26px;right:-54px;width:180px;opacity:.36;pointer-events:none}
+.sc-hero.sc-has-vis .sc-hero-vis{position:absolute;z-index:-1;top:-26px;right:0;width:180px;opacity:.36;pointer-events:none}
.sc-hero.sc-has-vis .sc-aura .sc-vis-mark{width:46px!important;height:46px!important}
.sc-hero.sc-has-vis .sc-aura .sc-vis-mark{width:46px!important;height:46px!important}
.sc-hero.sc-has-orbit{display:flex;flex-direction:column}
.sc-hero.sc-has-orbit{display:flex;flex-direction:column}
.sc-hero.sc-has-orbit .sc-hero-vis{order:-1;position:static;width:auto;opacity:1;pointer-events:auto;margin:0 0 6px}
.sc-hero.sc-has-orbit .sc-hero-vis{order:-1;position:static;width:auto;opacity:1;pointer-events:auto;margin:0 0 6px}
@@ -149,7 +149,7 @@ a.sc-stat:hover{border-color:var(--accent,#3b82f6)}
@@ -149,7 +149,7 @@ a.sc-stat:hover{border-color:var(--accent,#3b82f6)}
/* A list that is one sideways row on a phone (scroll snap, the next card peeking) and whatever the page made it on wide
/* A list that is one sideways row on a phone (scroll snap, the next card peeking) and whatever the page made it on wide
screens. Every item stays in the HTML. --sc-row-w sets a card's width (82% by default; 46% for small tiles). */
screens. Every item stays in the HTML. --sc-row-w sets a card's width (82% by default; 46% for small tiles). */
@media (max-width:599px){
@media (max-width:599px){
-.sc-row.sc-row{display:flex;flex-wrap:nowrap;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin-left:-16px;margin-right:-16px;padding:0 16px 4px;grid-template-columns:none}
+.sc-row.sc-row{position:relative;display:flex;flex-wrap:nowrap;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin-left:-16px;margin-right:-16px;padding:0 16px 4px;grid-template-columns:none}
.sc-row.sc-row::-webkit-scrollbar{display:none}
.sc-row.sc-row::-webkit-scrollbar{display:none}
.sc-row.sc-row>*{flex:0 0 var(--sc-row-w,82%);min-width:0;scroll-snap-align:start}
.sc-row.sc-row>*{flex:0 0 var(--sc-row-w,82%);min-width:0;scroll-snap-align:start}
.sc-fold-hint{display:none}
.sc-fold-hint{display:none}
@@ -447,7 +447,7 @@ a.sc-orb-node:hover .sc-orb-face>*:first-child,a.sc-orb-node:focus-visible .sc-o
@@ -447,7 +447,7 @@ a.sc-orb-node:hover .sc-orb-face>*:first-child,a.sc-orb-node:focus-visible .sc-o
html[data-ov-motion=reduced] .sc-ring,html[data-ov-motion=reduced] .sc-aura-glow,html[data-ov-motion=reduced] .sc-orb-ring,html[data-ov-motion=reduced] .sc-orb-face{animation:none}
html[data-ov-motion=reduced] .sc-ring,html[data-ov-motion=reduced] .sc-aura-glow,html[data-ov-motion=reduced] .sc-orb-ring,html[data-ov-motion=reduced] .sc-orb-face{animation:none}
@media (max-width:860px){
@media (max-width:860px){
.sc-hero.sc-has-vis{display:block}
.sc-hero.sc-has-vis{display:block}
-.sc-hero.sc-has-vis .sc-hero-vis{position:absolute;z-index:-1;top:-26px;right:-54px;width:180px;opacity:.36;pointer-events:none}
+.sc-hero.sc-has-vis .sc-hero-vis{position:absolute;z-index:-1;top:-26px;right:0;width:180px;opacity:.36;pointer-events:none}
.sc-hero.sc-has-vis .sc-aura .sc-vis-mark{width:46px!important;height:46px!important}
.sc-hero.sc-has-vis .sc-aura .sc-vis-mark{width:46px!important;height:46px!important}
.sc-hero.sc-has-orbit{display:flex;flex-direction:column}
.sc-hero.sc-has-orbit{display:flex;flex-direction:column}
.sc-hero.sc-has-orbit .sc-hero-vis{order:-1;position:static;width:auto;opacity:1;pointer-events:auto;margin:0 0 6px}
.sc-hero.sc-has-orbit .sc-hero-vis{order:-1;position:static;width:auto;opacity:1;pointer-events:auto;margin:0 0 6px}
@@ -569,7 +569,7 @@ a.sc-stat:hover{border-color:var(--accent,#3b82f6)}
@@ -569,7 +569,7 @@ a.sc-stat:hover{border-color:var(--accent,#3b82f6)}
/* A list that is one sideways row on a phone (scroll snap, the next card peeking) and whatever the page made it on wide
/* A list that is one sideways row on a phone (scroll snap, the next card peeking) and whatever the page made it on wide
screens. Every item stays in the HTML. --sc-row-w sets a card's width (82% by default; 46% for small tiles). */
screens. Every item stays in the HTML. --sc-row-w sets a card's width (82% by default; 46% for small tiles). */
@media (max-width:599px){
@media (max-width:599px){
-.sc-row.sc-row{display:flex;flex-wrap:nowrap;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin-left:-16px;margin-right:-16px;padding:0 16px 4px;grid-template-columns:none}
+.sc-row.sc-row{position:relative;display:flex;flex-wrap:nowrap;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin-left:-16px;margin-right:-16px;padding:0 16px 4px;grid-template-columns:none}
.sc-row.sc-row::-webkit-scrollbar{display:none}
.sc-row.sc-row::-webkit-scrollbar{display:none}
.sc-row.sc-row>*{flex:0 0 var(--sc-row-w,82%);min-width:0;scroll-snap-align:start}
.sc-row.sc-row>*{flex:0 0 var(--sc-row-w,82%);min-width:0;scroll-snap-align:start}
.sc-fold-hint{display:none}
.sc-fold-hint{display:none}
@@ -28,7 +28,8 @@ assert.ok(fo.includes('<div class="sc-fold-body"><section class="sc-sec"'), 'oth
@@ -28,7 +28,8 @@ assert.ok(fo.includes('<div class="sc-fold-body"><section class="sc-sec"'), 'oth
assert.ok(sc.fold({ title: 'T', html: '<p>x</p>', open: true }).includes('<details class="sc-fold" open>'), 'open on request');
assert.ok(sc.fold({ title: 'T', html: '<p>x</p>', open: true }).includes('<details class="sc-fold" open>'), 'open on request');
assert.strictEqual(sc.fold({ title: 'T', html: '' }), '', 'nothing to fold renders nothing');
assert.strictEqual(sc.fold({ title: 'T', html: '' }), '', 'nothing to fold renders nothing');
assert.strictEqual(sc.fold({ html: '<p>x</p>' }), '', 'a fold needs a heading');
assert.strictEqual(sc.fold({ html: '<p>x</p>' }), '', 'a fold needs a heading');
-assert.ok(sc.CSS.includes('.sc-row.sc-row{display:flex;flex-wrap:nowrap'), 'the phone row does not inherit a wrap');
+assert.ok(sc.CSS.includes('.sc-row.sc-row{position:relative;display:flex;flex-wrap:nowrap'), 'the phone row does not inherit a wrap, and holds what it clips (4.47.1)');
+assert.ok(/\.sc-hero\.sc-has-vis \.sc-hero-vis\{[^}]*right:0[;}]/.test(sc.CSS), 'the phone hero watermark stays inside the screen (4.47.1)');
// A set of six or nine apps is three a row; eight is four; seven keeps the grid's own columns.
// A set of six or nine apps is three a row; eight is four; seven keeps the grid's own columns.
const appsN = (n) => sc.apps({ items: Array.from({ length: n }, (_, i) => ({ name: `App ${i}`, href: `/a${i}` })) });
const appsN = (n) => sc.apps({ items: Array.from({ length: n }, (_, i) => ({ name: `App ${i}`, href: `/a${i}` })) });