Duo-ready
A book-style foldable opens into a nearly square screen with a crease down the middle. Taps on the crease register badly, so any small control that straddles the centre line is a lost click. This skill reads the site at every pose, finds those controls, moves them with the smallest possible CSS change, proves the fix, and draws the picture that shows a client why it mattered.
What is known, and what is not
- Geometry. iPhone Duo opened is 951 x 669 points by App Store Connect's screenshot size, or 890 x 626 by the spec sheet's panel resolution. Until the device settles it, check both. Closed, it is 466 x 678. The crease is the horizontal centre of the opened screen.
- No web API. As of Safari 27, WebKit ships neither the Viewport Segments API
nor the Device Posture API. A page cannot ask where the fold is; it has to
assume the centre. Native apps get
UIViewReservedRegion; the web gets nothing. - Apple's rule. Scrolling content - articles, feeds, long rows - may cross the crease. Controls must not.
Details and sources: references/duo-geometry.md.
Workflow
1. Read the site at every pose
node scripts/duo-check.mjs https://example.com https://example.com/pricing --out duo-report --shots
Needs Node 22+ and Google Chrome (CHROME_PATH overrides). It opens every URL at
six poses with touch input emulated, scrolls the whole page so lazy sections
load, and writes duo-report.md, duo-report.json and, with --shots, one
screenshot per hit with the centre line dashed in. Exit code 1 means work to do.
Read every template that carries controls: home, a product or pricing page, the checkout or signup flow, a blog post.
2. Triage
Read duo-report.md and sort each finding:
| Finding | Action |
|---|---|
Control narrower than 240px straddling the centre at open-890 or open-951 |
Must move |
| Row or card wider than 240px crossing the centre | Leave it - Apple allows this |
| Any pose scrolls sideways | Fix regardless of the fold |
3. Fix
Put every change in one block at the end of the stylesheet, so it touches nothing else and can be removed in one piece when Safari ships a real API:
/* Opened book-style foldable: the crease runs down the middle of the page.
No web API reports it yet, so this covers both published iPhone Duo widths. */
@media (min-width: 880px) and (max-width: 962px) and (pointer: coarse) {
/* fixes go here */
}
The range catches no iPad and not the Galaxy Z Fold 7's inner screen (984px). A few Android tablets held landscape may match; every fix below is harmless there.
Prefer moves that hold at any width over caps tuned to one:
| Pattern on the crease | Width-independent fix |
|---|---|
| Grid with an odd number of columns | Make it even: grid-template-columns: repeat(4, 1fr) puts a gutter on the centre |
| Inline row of keys, chips or links | padding-right: calc(50% + 2.5rem) on the row keeps its rule full width and wraps the items short of the centre |
| Row of fixed-size icons whose split lands near the centre | Widen the gap until the split between two items sits on the centre, then check at both widths |
| Field with its button beside it | Stack: flex-direction: column; align-items: flex-start |
| Two-column card with its button at the far end | One column: grid-template-columns: minmax(0, 1fr), button justify-self: start |
| Side-by-side pair of buttons | flex-direction: column; align-items: flex-start |
| List row with a side column pushing controls right | Use the row's own narrow-screen stacked form inside the block |
Do not hide content, shrink tap targets, or change anything outside the block.
One rule outside it, for the closed screen: the Dynamic Island stands on one side
there, so left and right safe areas differ. With viewport-fit=cover, give
full-bleed fixed bars padding-left: env(safe-area-inset-left, 0px) and the
right-hand equivalent, written inline rather than through a custom property.
4. Verify
Run the check again. The bar is zero narrow controls on the crease at both
open-890 and open-951, and no sideways scroll at any pose. A fix that holds
at one width and not the other is not done.
5. Show it
node scripts/duo-mockup.mjs https://example.com --scroll ".pricing" --label "Before" --out before.png
# apply and deploy the fix, then:
node scripts/duo-mockup.mjs https://example.com --scroll ".pricing" --label "After" --out after.png
--scroll takes a CSS selector or a pixel offset; --width 890 renders the
narrower opened size. Capture "Before" before deploying, not after.
6. Check in-app browsers too
Links from Instagram, TikTok, Telegram and LinkedIn open in the app's own
WebKit view, not in Safari, and fixed headers behave differently there. Host
probe/vp.html and probe/vp.js anywhere, open the page from the app, and read
the numbers. The one rule that matters: never pad a fixed header by
env(safe-area-inset-top) - measured on iOS 26.5 it is 0px in Safari and 62px
in an in-app view whose toolbar already covers that band, so the padding opens a
dead strip above the navigation.
On a real opened Duo, the same probe shows the true viewport width and a dashed centre line: one photo of the screen settles 890 against 951 and shows whether the centre and the physical crease coincide.
7. Report honestly
State that the checks ran in emulation, which widths were checked, and that the result should be confirmed on the device. Name the date. When the Viewport Segments or Device Posture API ships in Safari, replace the width range with a real query and delete the guesswork.