test(mobile): follow the 600px phone cut on the Duo branch

Rebased over #390, which moved the phone tier's cutoff from 430px to
600px. The palette's compound fold rule now lives in the 600-768px band
mobile.css pads, the cascade samples the palette inside that band, and
the closed iPhone Duo (466pt) is a phone rather than a small tablet while
the open one (626pt) stays a tablet. Comments in both stylesheets, the
device registry, CLAUDE.md and architecture-invariants say 600.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-09-14 16:02:02 +02:00
parent 2f9fc72252
commit 21dcec5d24
6 changed files with 23 additions and 22 deletions
+1 -1
View File
@@ -326,7 +326,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
**Foldable settings identity**: responsive layout is width-driven via `MobileDetection.getDeviceType()`, but the localStorage namespace uses `MobileDetection.isHandheldDevice()` so an unfolded Android foldable keeps `codeman-app-settings-mobile`. ⚠️ Do not switch per-device settings namespaces from instantaneous viewport width: a posture-triggered WebView reload would lose opt-in UI. Regression profile: `OPPO Find N5 (unfolded)` in `test/mobile/devices.ts`. → [architecture-invariants#foldable-settings-identity](docs/architecture-invariants.md#foldable-settings-identity) **Foldable settings identity**: responsive layout is width-driven via `MobileDetection.getDeviceType()`, but the localStorage namespace uses `MobileDetection.isHandheldDevice()` so an unfolded Android foldable keeps `codeman-app-settings-mobile`. ⚠️ Do not switch per-device settings namespaces from instantaneous viewport width: a posture-triggered WebView reload would lose opt-in UI. Regression profile: `OPPO Find N5 (unfolded)` in `test/mobile/devices.ts`. → [architecture-invariants#foldable-settings-identity](docs/architecture-invariants.md#foldable-settings-identity)
**Folding devices: a fold is not a keyboard, and dialogs avoid the hinge** (Apple's [Designing for iPhone Duo](https://developer.apple.com/design/human-interface-guidelines/designing-for-iphone-duo)): ⚠️ **A visual-viewport resize that changes the WIDTH is the device changing shape** (a rotation, or a foldable opening or closing) **and is never the virtual keyboard**, which only ever takes height. `handleViewportResize()` read any height drop over 150px as the keyboard appearing, so closing an iPhone Duo (626→466pt wide, 890→678pt tall) latched `keyboardVisible` with no keyboard on screen: the accessory bar appeared, `main` grew 84px of dead padding, and `updateAppHeight()` (which bails while the keyboard is up) stopped refreshing `--app-height`. The latch is STICKY, since clearing it needs the height back within 100px of a baseline belonging to a display the user is no longer looking at, so it survived until the device was opened again, and rotating any phone hit it too. The shape branch re-baselines instead, which is also what lets a keyboard opened AFTER the fold be detected. ⚠️ `init()` must seed `lastViewportWidth`, or the very first resize reads as a shape change and swallows a real keyboard. ⚠️ **The hinge is a reserved region.** The CSS Viewport Segments media features report two segments only while a foldable is actually bent, and `--fold-inline-end`/`--fold-block-end` (styles.css) measure the strip to keep clear: `0px` on everything else, so the rules are inert by construction rather than by a branch. Codeman's seven centred overlays are all `position: fixed; inset: 0` flex boxes, and each shrinks its CONTENT box with padding rather than the box itself, so the backdrop still covers the far side of the fold and still swallows taps there. ⚠️ Each rule RE-STATES the overlay's own gutter (a later `padding-right` longhand beats the earlier `padding` shorthand it composes with), and the palette needs the compound `.modal.command-palette-modal` because mobile.css loads later and pads it with a shorthand under 768px. `test/foldable-layout.test.ts` DERIVES the overlay list from the stylesheet and compares both numbers, so a new overlay or a moved gutter fails there instead of on hardware nobody has. ⚠️ Physical sides, not logical ones: dialogs sit in the LEFT segment (the TOP one in tabletop pose) in every language, because the HIG keeps Duo's side controls on the same physical edge in RTL. ⚠️ **With the keyboard up, a shape change baselines to `window.innerHeight`** (the layout viewport, keyboard-free on both engines), never the shrunk visual height: the shrunk baseline made the settle event that follows every rotation or fold read as the keyboard closing, and the layout could not recover, since no further drop could re-arm the show branch. ⚠️ **A base gutter that a LATER `@media` block overrides needs its own fold restatement in that block, on a ZERO base**: the phone-width path picker and path preview drop to `padding: 0` under 600px, and the unconditional fold rules at the end of the file put 16px and 18px back on every phone (measured at 393 and 500). The palette's compound rule lives INSIDE the 430-768px band whose mobile.css shorthand it composes with, because outside it there is no side gutter and the addition pushed the shell 6px off centre; the response viewer's tabletop cap has a twin at the end of mobile.css, whose 430px block otherwise outranks it. `test/foldable-layout.test.ts` simulates the cascade across BOTH stylesheets at every breakpoint, with and without the fold rules, so a moved gutter or an unscoped composition fails there rather than on hardware nobody has. Profiles: `iPhone Duo (outer)` / `iPhone Duo (inner)` in `test/mobile/devices.ts`; unit coverage in `test/viewport-shape-change.test.ts`. → [architecture-invariants#folding-devices](docs/architecture-invariants.md#folding-devices) **Folding devices: a fold is not a keyboard, and dialogs avoid the hinge** (Apple's [Designing for iPhone Duo](https://developer.apple.com/design/human-interface-guidelines/designing-for-iphone-duo)): ⚠️ **A visual-viewport resize that changes the WIDTH is the device changing shape** (a rotation, or a foldable opening or closing) **and is never the virtual keyboard**, which only ever takes height. `handleViewportResize()` read any height drop over 150px as the keyboard appearing, so closing an iPhone Duo (626→466pt wide, 890→678pt tall) latched `keyboardVisible` with no keyboard on screen: the accessory bar appeared, `main` grew 84px of dead padding, and `updateAppHeight()` (which bails while the keyboard is up) stopped refreshing `--app-height`. The latch is STICKY, since clearing it needs the height back within 100px of a baseline belonging to a display the user is no longer looking at, so it survived until the device was opened again, and rotating any phone hit it too. The shape branch re-baselines instead, which is also what lets a keyboard opened AFTER the fold be detected. ⚠️ `init()` must seed `lastViewportWidth`, or the very first resize reads as a shape change and swallows a real keyboard. ⚠️ **The hinge is a reserved region.** The CSS Viewport Segments media features report two segments only while a foldable is actually bent, and `--fold-inline-end`/`--fold-block-end` (styles.css) measure the strip to keep clear: `0px` on everything else, so the rules are inert by construction rather than by a branch. Codeman's seven centred overlays are all `position: fixed; inset: 0` flex boxes, and each shrinks its CONTENT box with padding rather than the box itself, so the backdrop still covers the far side of the fold and still swallows taps there. ⚠️ Each rule RE-STATES the overlay's own gutter (a later `padding-right` longhand beats the earlier `padding` shorthand it composes with), and the palette needs the compound `.modal.command-palette-modal` because mobile.css loads later and pads it with a shorthand under 768px. `test/foldable-layout.test.ts` DERIVES the overlay list from the stylesheet and compares both numbers, so a new overlay or a moved gutter fails there instead of on hardware nobody has. ⚠️ Physical sides, not logical ones: dialogs sit in the LEFT segment (the TOP one in tabletop pose) in every language, because the HIG keeps Duo's side controls on the same physical edge in RTL. ⚠️ **With the keyboard up, a shape change baselines to `window.innerHeight`** (the layout viewport, keyboard-free on both engines), never the shrunk visual height: the shrunk baseline made the settle event that follows every rotation or fold read as the keyboard closing, and the layout could not recover, since no further drop could re-arm the show branch. ⚠️ **A base gutter that a LATER `@media` block overrides needs its own fold restatement in that block, on a ZERO base**: the phone-width path picker and path preview drop to `padding: 0` under 600px, and the unconditional fold rules at the end of the file put 16px and 18px back on every phone (measured at 393 and 500). The palette's compound rule lives INSIDE the 600-768px band whose mobile.css shorthand it composes with, because outside it there is no side gutter and the addition pushed the shell 6px off centre; the response viewer's tabletop cap has a twin at the end of mobile.css, whose phone block (under 600px) otherwise outranks it. `test/foldable-layout.test.ts` simulates the cascade across BOTH stylesheets at every breakpoint, with and without the fold rules, so a moved gutter or an unscoped composition fails there rather than on hardware nobody has. Profiles: `iPhone Duo (outer)` / `iPhone Duo (inner)` in `test/mobile/devices.ts`; unit coverage in `test/viewport-shape-change.test.ts`. → [architecture-invariants#folding-devices](docs/architecture-invariants.md#folding-devices)
**WebGL renderer toggle** (`webglRendererEnabled`, per-device): the GPU-stall watchdog's sticky `codeman-webgl-disabled` marker survives page loads and is cleared only by an explicit OFF→ON save or `?webgl=force`. `?nowebgl` forces the DOM renderer per-load. → [architecture-invariants#webgl-renderer-toggle](docs/architecture-invariants.md#webgl-renderer-toggle) **WebGL renderer toggle** (`webglRendererEnabled`, per-device): the GPU-stall watchdog's sticky `codeman-webgl-disabled` marker survives page loads and is cleared only by an explicit OFF→ON save or `?webgl=force`. `?nowebgl` forces the DOM renderer per-load. → [architecture-invariants#webgl-renderer-toggle](docs/architecture-invariants.md#webgl-renderer-toggle)
+1 -1
View File
@@ -404,7 +404,7 @@ Anatomy: `.set-shell` → `.set-shell-head` (title + `.set-head-actions`) + `.se
**With the keyboard up, the new baseline is `window.innerHeight`, never the shrunk visual height.** The page sets no `interactive-widget`, so the default resizes-visual mode holds on both engines: the keyboard shrinks only the visual viewport and the layout viewport stays the display's full height (`updateLayoutForKeyboard()` relies on the same fact). The first version baselined to the shrunk height, which made `heightDiff` 0, so the settle event the OS animation fires at the new width (or any later address-bar drift) satisfied the hide branch and ran `onKeyboardHide()` with the keyboard still on screen: accessory bar hidden, the toolbar lift dropped, `main`'s padding cleared. It could not recover, because no further 150px drop can re-arm the show branch against a baseline already sitting at the shrunk height. Reproduced against the real handler in the vm harness with both the fold flavour (626x590 → 466x378 → 466x378) and the rotation flavour (393x359 → 852x150 → 852x160); `test/viewport-shape-change.test.ts` models the two heights separately and pins both. **With the keyboard up, the new baseline is `window.innerHeight`, never the shrunk visual height.** The page sets no `interactive-widget`, so the default resizes-visual mode holds on both engines: the keyboard shrinks only the visual viewport and the layout viewport stays the display's full height (`updateLayoutForKeyboard()` relies on the same fact). The first version baselined to the shrunk height, which made `heightDiff` 0, so the settle event the OS animation fires at the new width (or any later address-bar drift) satisfied the hide branch and ran `onKeyboardHide()` with the keyboard still on screen: accessory bar hidden, the toolbar lift dropped, `main`'s padding cleared. It could not recover, because no further 150px drop can re-arm the show branch against a baseline already sitting at the shrunk height. Reproduced against the real handler in the vm harness with both the fold flavour (626x590 → 466x378 → 466x378) and the rotation flavour (393x359 → 852x150 → 852x160); `test/viewport-shape-change.test.ts` models the two heights separately and pins both.
**The hinge is a reserved region, and the CSS is inert by construction.** The CSS Viewport Segments media features report two segments only while a foldable is actually bent; `--fold-inline-end` / `--fold-block-end` (end of styles.css) measure the strip to keep clear from the LEADING segment (`env(viewport-segment-right 0 0)` and `env(viewport-segment-bottom 0 0)`, physical sides in every text direction) and are `0px` on everything else. The seven centred overlays are all `position: fixed; inset: 0` flex boxes; each shrinks its CONTENT box with padding rather than the box itself, so the backdrop still covers the far side of the fold and still swallows taps there. The cascade traps, each measured in headless Chromium against styles.css + mobile.css in index.html link order: (1) a later `padding-right` longhand beats the earlier `padding` shorthand it composes with, so each fold rule re-states the overlay's own gutter; (2) a base gutter that a LATER `@media` block overrides needs its own fold restatement in that block on a ZERO base, since the unconditional rules at the end of the file otherwise put the gutter back (the phone path picker and path preview drop to `padding: 0` under 600px and came back at 16px and 18px on every phone); (3) a compound rule written to outrank a mobile.css shorthand must be scoped to the band where that shorthand applies, because outside it there is no gutter to compose with (the palette's `.modal.command-palette-modal` added 0.75rem at 393, 900 and 1400px and pushed the shell 6px off centre, and inside the band it lost its bottom gutter to the shorthand until it restated that side too); (4) mobile.css loads AFTER styles.css, so a same-specificity rule there wins under its own breakpoint (the response viewer's `max-height: 92dvh` at 430px beat the tabletop cap, which now has a twin at the end of mobile.css). `test/foldable-layout.test.ts` DERIVES the overlay list from the stylesheet, simulates the cascade across both files at every breakpoint with and without the fold rules, and requires the two results to differ by exactly the fold strip, so each of the four fails there instead of on hardware nobody has. **The hinge is a reserved region, and the CSS is inert by construction.** The CSS Viewport Segments media features report two segments only while a foldable is actually bent; `--fold-inline-end` / `--fold-block-end` (end of styles.css) measure the strip to keep clear from the LEADING segment (`env(viewport-segment-right 0 0)` and `env(viewport-segment-bottom 0 0)`, physical sides in every text direction) and are `0px` on everything else. The seven centred overlays are all `position: fixed; inset: 0` flex boxes; each shrinks its CONTENT box with padding rather than the box itself, so the backdrop still covers the far side of the fold and still swallows taps there. The cascade traps, each measured in headless Chromium against styles.css + mobile.css in index.html link order: (1) a later `padding-right` longhand beats the earlier `padding` shorthand it composes with, so each fold rule re-states the overlay's own gutter; (2) a base gutter that a LATER `@media` block overrides needs its own fold restatement in that block on a ZERO base, since the unconditional rules at the end of the file otherwise put the gutter back (the phone path picker and path preview drop to `padding: 0` under 600px and came back at 16px and 18px on every phone); (3) a compound rule written to outrank a mobile.css shorthand must be scoped to the band where that shorthand applies, because outside it there is no gutter to compose with (the palette's `.modal.command-palette-modal` added 0.75rem at 393, 900 and 1400px and pushed the shell 6px off centre, and inside the band it lost its bottom gutter to the shorthand until it restated that side too); (4) mobile.css loads AFTER styles.css, so a same-specificity rule there wins under its own breakpoint (the response viewer's `max-height: 92dvh` under 600px beat the tabletop cap, which now has a twin at the end of mobile.css). `test/foldable-layout.test.ts` DERIVES the overlay list from the stylesheet, simulates the cascade across both files at every breakpoint with and without the fold rules, and requires the two results to differ by exactly the fold strip, so each of the four fails there instead of on hardware nobody has.
## Security layers ## Security layers
+1 -1
View File
@@ -3853,7 +3853,7 @@ html[data-session-list="sidebar"] .session-sidebar .session-tab .tab-close {
/* Folding devices, tabletop pose: cap the response viewer to the bottom /* Folding devices, tabletop pose: cap the response viewer to the bottom
segment. Twin of the rule at the end of styles.css, needed here because the segment. Twin of the rule at the end of styles.css, needed here because the
430px block above sets `max-height: 92dvh` on the same selector at the same phone block (under 600px) above sets `max-height: 92dvh` on the same selector at the same
specificity, and this file loads later, so the styles.css copy loses on a specificity, and this file loads later, so the styles.css copy loses on a
phone-width foldable. Keep the value identical; test/foldable-layout.test.ts phone-width foldable. Keep the value identical; test/foldable-layout.test.ts
compares them. */ compares them. */
+2 -2
View File
@@ -18128,14 +18128,14 @@ html[data-session-list="sidebar"][data-sidebar="collapsed"] .btn-sidebar-toggle
} }
/* The palette is a .modal, so the generic rule above covers it everywhere /* The palette is a .modal, so the generic rule above covers it everywhere
EXCEPT the 430-768px band, where mobile.css (loaded after this file) pads EXCEPT the 600-768px band, where mobile.css (loaded after this file) pads
it with the SHORTHAND `10vh 0.75rem 0`: that shorthand beats the generic it with the SHORTHAND `10vh 0.75rem 0`: that shorthand beats the generic
rule on both sides, so this compound rule (0,2,0) restates that band's own rule on both sides, so this compound rule (0,2,0) restates that band's own
gutters, 0.75rem at the side and none at the bottom, plus the fold strips. gutters, 0.75rem at the side and none at the bottom, plus the fold strips.
Scoped to the same band on purpose: unscoped, it ADDED 0.75rem on every Scoped to the same band on purpose: unscoped, it ADDED 0.75rem on every
other width, where the palette has no side gutter to compose with, and other width, where the palette has no side gutter to compose with, and
pushed the shell 6px off centre (measured at 393, 900 and 1400). */ pushed the shell 6px off centre (measured at 393, 900 and 1400). */
@media (max-width: 768px) and (min-width: 430px) { @media (max-width: 768px) and (min-width: 600px) {
.modal.command-palette-modal { .modal.command-palette-modal {
padding-right: calc(0.75rem + var(--fold-inline-end)); padding-right: calc(0.75rem + var(--fold-inline-end));
padding-bottom: var(--fold-block-end); padding-bottom: var(--fold-block-end);
+13 -12
View File
@@ -22,8 +22,8 @@
* 3. The gutter an overlay ends up with is a CASCADE across two files and * 3. The gutter an overlay ends up with is a CASCADE across two files and
* several breakpoints, not one rule: a later @media block can zero it (the * several breakpoints, not one rule: a later @media block can zero it (the
* phone path picker under 600px), mobile.css can replace it with a * phone path picker under 600px), mobile.css can replace it with a
* shorthand (the palette between 430 and 768px) and, loading later, can * shorthand (the palette between 600 and 768px) and, loading later, can
* outrank a same-specificity rule (the response viewer under 430px). So the * outrank a same-specificity rule (the response viewer under 600px). So the
* cascade is simulated at every breakpoint, once with the fold rules and * cascade is simulated at every breakpoint, once with the fold rules and
* once without, and the two results must differ by exactly the fold strip. * once without, and the two results must differ by exactly the fold strip.
* Each of the three shipped once with the top-level-only comparison green. * Each of the three shipped once with the top-level-only comparison green.
@@ -325,15 +325,15 @@ describe('fold reserved region: every centred overlay is covered', () => {
// Chromium (styles.css + mobile.css in index.html link order): the phone // Chromium (styles.css + mobile.css in index.html link order): the phone
// path picker is flush under 600px and keeps its 16px gutter above it; // path picker is flush under 600px and keeps its 16px gutter above it;
// the palette carries mobile.css's 0.75rem side gutter only inside the // the palette carries mobile.css's 0.75rem side gutter only inside the
// 430-768px band. A model that cannot reproduce these numbers proves // 600-768px band. A model that cannot reproduce these numbers proves
// nothing about the fold rules built on top of them. // nothing about the fold rules built on top of them.
const picker = ['path-picker-overlay']; const picker = ['path-picker-overlay'];
expect(cascadedPadding(picker, 'right', 393, false)).toBe('0'); expect(cascadedPadding(picker, 'right', 393, false)).toBe('0');
expect(cascadedPadding(picker, 'right', 626, false)).toBe('16px'); expect(cascadedPadding(picker, 'right', 626, false)).toBe('16px');
const palette = ELEMENTS.at(-1)!.classes; const palette = ELEMENTS.at(-1)!.classes;
expect(cascadedPadding(palette, 'right', 393, false)).toBeNull(); expect(cascadedPadding(palette, 'right', 393, false)).toBeNull();
expect(cascadedPadding(palette, 'right', 500, false)).toBe('0.75rem'); expect(cascadedPadding(palette, 'right', 626, false)).toBe('0.75rem');
expect(cascadedPadding(palette, 'bottom', 500, false)).toBe('0'); expect(cascadedPadding(palette, 'bottom', 626, false)).toBe('0');
expect(cascadedPadding(palette, 'right', 900, false)).toBeNull(); expect(cascadedPadding(palette, 'right', 900, false)).toBeNull();
}); });
@@ -353,7 +353,7 @@ describe('fold reserved region: every centred overlay is covered', () => {
it('composes with the padding shorthand mobile.css gives the command palette, inside that band only', () => { it('composes with the padding shorthand mobile.css gives the command palette, inside that band only', () => {
// mobile.css loads after styles.css and sets a `padding` SHORTHAND on // mobile.css loads after styles.css and sets a `padding` SHORTHAND on
// .command-palette-modal between 430 and 768px, exactly where a folding // .command-palette-modal between 600 and 768px, exactly where a folding
// phone lives, so a bare .command-palette-modal rule would lose to it and // phone lives, so a bare .command-palette-modal rule would lose to it and
// the compound rule has to restate BOTH of that band's gutters. Scoped to // the compound rule has to restate BOTH of that band's gutters. Scoped to
// the same band: unscoped, it added 0.75rem where the palette has no side // the same band: unscoped, it added 0.75rem where the palette has no side
@@ -374,7 +374,7 @@ describe('fold reserved region: every centred overlay is covered', () => {
it('gives the response viewer cap a later twin in mobile.css', () => { it('gives the response viewer cap a later twin in mobile.css', () => {
// mobile.css sets `max-height` on .response-viewer at the same specificity // mobile.css sets `max-height` on .response-viewer at the same specificity
// under 430px and loads later, so the styles.css cap alone loses on a // under 600px and loads later, so the styles.css cap alone loses on a
// phone-width foldable. The twin must come after that rule and carry the // phone-width foldable. The twin must come after that rule and carry the
// identical value. // identical value.
const capOf = (root: postcss.Root) => const capOf = (root: postcss.Root) =>
@@ -441,11 +441,12 @@ describe('a fold never changes which settings the device is using', () => {
expect(detectionFor(postures[2].ua, postures[2].w).getDeviceType()).toBe('mobile'); expect(detectionFor(postures[2].ua, postures[2].w).getDeviceType()).toBe('mobile');
}); });
it('gives both iPhone Duo displays the tablet layout', () => { it('gives the closed iPhone Duo the phone layout and the open one the tablet layout', () => {
// 466 and 626 both sit above the 430px phone cut and below 768. Deliberate // 466 sits under the 600px phone cut (#390 moved it up from 430) and 626
// (see shouldUseMobileOverview), and pinned because a 5.4" phone landing in // above it, below 768. Deliberate (see shouldUseMobileOverview), and pinned
// the tablet band is the kind of thing that looks like a bug later. // because the tier flipping under a fold is the kind of thing that looks
expect(detectionFor(postures[0].ua, postures[0].w).getDeviceType()).toBe('tablet'); // like a bug later: closed, the Duo is a phone; open, it is a small tablet.
expect(detectionFor(postures[0].ua, postures[0].w).getDeviceType()).toBe('mobile');
expect(detectionFor(postures[1].ua, postures[1].w).getDeviceType()).toBe('tablet'); expect(detectionFor(postures[1].ua, postures[1].w).getDeviceType()).toBe('tablet');
}); });
}); });
+5 -5
View File
@@ -292,11 +292,11 @@ const customEntries: DeviceEntry[] = [
// in both axes. The registry's other foldable (Find N5) uses the full // in both axes. The registry's other foldable (Find N5) uses the full
// viewport for the same reason. // viewport for the same reason.
// //
// The PAIR is what earns its place here. Both postures land in the tablet // The PAIR is what earns its place here. The postures straddle the 600px
// band (466 and 626 are each above the 430px phone cut and below 768), so a // phone cut (#390): closed, 466 is a phone; open, 626 is a small tablet, so
// 5.4" phone in someone's hand gets the roomier layout. Deliberate, per the // the layout tier flips with the fold. Deliberate, per the note on
// note on shouldUseMobileOverview(), and worth a profile precisely because it // shouldUseMobileOverview(), and worth a profile precisely because it is
// is easy to regress into a phone-width assumption. What must NOT move with // easy to regress into a single-width assumption. What must NOT move with
// the fold is the per-device settings identity, which is UA-based and // the fold is the per-device settings identity, which is UA-based and
// therefore identical across the two; test/mobile/settings.test.ts pins it. // therefore identical across the two; test/mobile/settings.test.ts pins it.
custom('iPhone Duo (outer)', 466, 678, 3, IOS_MOBILE_UA('26_0'), true), custom('iPhone Duo (outer)', 466, 678, 3, IOS_MOBILE_UA('26_0'), true),