@brunosps00/dev-workflow 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +72 -0
  3. package/README.md +7 -4
  4. package/lib/doctor.js +17 -2
  5. package/lib/init.js +16 -6
  6. package/lib/migrate-skills.js +189 -39
  7. package/lib/removed-bundled-skills.js +4 -2
  8. package/lib/skill-registry.js +12 -2
  9. package/lib/uninstall.js +61 -5
  10. package/package.json +1 -1
  11. package/scaffold/en/agent-instructions.md +1 -1
  12. package/scaffold/en/commands/dw-brainstorm.md +2 -2
  13. package/scaffold/en/commands/dw-bugfix.md +1 -1
  14. package/scaffold/en/commands/dw-commit.md +1 -1
  15. package/scaffold/en/commands/dw-dockerize.md +2 -2
  16. package/scaffold/en/commands/dw-functional-doc.md +1 -1
  17. package/scaffold/en/commands/dw-harness-audit.md +1 -1
  18. package/scaffold/en/commands/dw-intel.md +1 -1
  19. package/scaffold/en/commands/dw-learn.md +1 -1
  20. package/scaffold/en/commands/dw-new-project.md +3 -3
  21. package/scaffold/en/commands/dw-plan.md +2 -2
  22. package/scaffold/en/commands/dw-qa.md +6 -5
  23. package/scaffold/en/commands/dw-redesign-ui.md +3 -3
  24. package/scaffold/en/commands/dw-review.md +2 -2
  25. package/scaffold/en/commands/dw-skill-health.md +24 -1
  26. package/scaffold/en/commands/dw-update.md +28 -11
  27. package/scaffold/en/references/invariants.md +4 -0
  28. package/scaffold/en/templates/project-onepager.md +1 -1
  29. package/scaffold/pt-br/agent-instructions.md +1 -1
  30. package/scaffold/pt-br/commands/dw-brainstorm.md +2 -2
  31. package/scaffold/pt-br/commands/dw-bugfix.md +1 -1
  32. package/scaffold/pt-br/commands/dw-commit.md +1 -1
  33. package/scaffold/pt-br/commands/dw-dockerize.md +2 -2
  34. package/scaffold/pt-br/commands/dw-functional-doc.md +1 -1
  35. package/scaffold/pt-br/commands/dw-harness-audit.md +1 -1
  36. package/scaffold/pt-br/commands/dw-intel.md +1 -1
  37. package/scaffold/pt-br/commands/dw-learn.md +1 -1
  38. package/scaffold/pt-br/commands/dw-new-project.md +3 -3
  39. package/scaffold/pt-br/commands/dw-plan.md +2 -2
  40. package/scaffold/pt-br/commands/dw-qa.md +6 -5
  41. package/scaffold/pt-br/commands/dw-redesign-ui.md +3 -3
  42. package/scaffold/pt-br/commands/dw-review.md +2 -2
  43. package/scaffold/pt-br/commands/dw-skill-health.md +24 -1
  44. package/scaffold/pt-br/commands/dw-update.md +28 -11
  45. package/scaffold/pt-br/references/invariants.md +5 -0
  46. package/scaffold/pt-br/templates/project-onepager.md +1 -1
  47. package/scaffold/scripts/hooks/session-cost.mjs +62 -21
  48. package/scaffold/skills/docker-compose-recipes/references/compose-composition.md +1 -1
  49. package/scaffold/skills/dw-llm-eval/SKILL.md +3 -1
  50. package/scaffold/skills/dw-llm-eval/references/integration-with-dev-workflow-commands-detail.md +2 -2
  51. package/scaffold/skills/dw-llm-eval/references/reference-dataset.md +57 -3
  52. package/scaffold/skills/dw-ui-discipline/SKILL.md +8 -5
  53. package/scaffold/skills/dw-ui-discipline/references/accessibility-floor.md +20 -8
  54. package/scaffold/skills/dw-ui-discipline/references/content-stress.md +246 -0
  55. package/scaffold/skills/dw-ui-discipline/references/motion.md +205 -35
  56. package/scaffold/skills/dw-ui-discipline/references/responsive.md +44 -2
@@ -1,52 +1,222 @@
1
- # Motion — easing, duration, stagger, reduced motion
1
+ # Motion — whether to animate, easing, duration, physicality, performance
2
2
 
3
- > Patterns adapted from [impeccable](https://github.com/pbakaus/impeccable) (Apache-2.0, derived from
4
- > Anthropic's frontend-design skill). Read when adding animation or transitions.
3
+ > First version adapted from [impeccable](https://github.com/pbakaus/impeccable) (Apache-2.0, derived from
4
+ > Anthropic's frontend-design skill). Rewritten 2026-11 from the `emil-design-eng`, `review-animations`,
5
+ > `animation-performance` and `remove-animations` skills in
6
+ > [`emilkowalski/skills`](https://github.com/emilkowalski/skills) (MIT © 2026 Emil Kowalski): the
7
+ > frequency gate, easing table, curves, physicality rules, escalation triggers and removal gate come from
8
+ > there. Read when adding, reviewing or removing animation or transitions.
5
9
 
6
- ## Motion conveys state, not decoration
10
+ Project motion tokens (`DESIGN.md`, CSS variables, Tailwind config) win over the values below. These are
11
+ the defaults when the project has none, and the bar when reviewing motion it already has.
7
12
 
8
- Animate to explain a change: where did this come from, what just happened, where did it go. If a motion
9
- doesn't answer one of those, cut it. Decorative motion is a visual-slop pattern.
13
+ ## 1. Should it animate at all?
10
14
 
11
- ## Easing — avoid bounce/elastic
15
+ Decide this before choosing any curve. Frequency first:
12
16
 
13
- Real objects decelerate smoothly. Bounce and elastic easing feel dated and tacky (the detector flags
14
- `bounce-easing` / spring overshoot like `cubic-bezier(0.68,-0.55,0.27,1.55)`).
17
+ | How often the user sees it | Decision |
18
+ |---|---|
19
+ | 100+ times a day (keyboard shortcuts, command palette, tab switches, in-app route changes, list rows) | No animation. Any duration reads as delay. |
20
+ | Tens of times a day (hover, frequent toggles, list expansion) | Remove, or reduce to near-imperceptible |
21
+ | Occasional (modals, drawers, toasts) | Standard motion |
22
+ | Rare or first-time (onboarding, success, empty state) | Room for delight |
15
23
 
16
- - **Enter** (element appears): ease-out — fast start, soft landing. `cubic-bezier(0.16, 1, 0.3, 1)`
17
- (ease-out-expo) or `ease-out`.
18
- - **Exit** (element leaves): ease-in — accelerate away. `cubic-bezier(0.4, 0, 1, 1)`.
19
- - **Move/resize on screen:** ease-in-out (standard) `cubic-bezier(0.4, 0, 0.2, 1)`.
20
- - Never linear for UI (only for continuous things like spinners/marquees).
24
+ **Keyboard-initiated actions never animate.** They repeat hundreds of times a day.
21
25
 
22
- ## Duration
26
+ Then purpose. Valid purposes: spatial consistency (where did this come from / go), state indication,
27
+ explanation, feedback, preventing a jarring change. "It looks nice" on a frequently seen element is not a
28
+ purpose. Functional data (charts, tables, numbers) does not re-animate on every mount or refresh; it
29
+ animates only to show a real change in value.
23
30
 
24
- - Micro (hover, small fades): 100–150ms.
25
- - Standard (dropdowns, toggles, cards): 200–300ms.
26
- - Large (page/route, full-screen sheets): 300–500ms.
27
- - Over ~500ms feels sluggish; under ~80ms is imperceptible (just snap with no transition).
31
+ Deleting everything is also wrong: a toast that would otherwise teleport in, or a panel that needs to show
32
+ where it came from, earns its motion.
28
33
 
29
- ## Stagger
34
+ ## 2. Easing
30
35
 
31
- When several items enter together, stagger them 20–50ms apart so the eye follows the sequence — but cap
32
- total stagger (don't make the 12th list item wait 600ms). Stagger entrances, not exits.
36
+ | Motion | Easing |
37
+ |---|---|
38
+ | Entering or exiting | `ease-out` (starts fast, feels responsive) |
39
+ | Moving or morphing on screen | `ease-in-out` |
40
+ | Hover, color change | `ease` |
41
+ | Constant motion (marquee, progress, spinner) | `linear` |
42
+ | Default | `ease-out` |
33
43
 
34
- ## What to animate (performance)
44
+ **Never `ease-in` on UI**, exits included. It starts slow and delays the exact moment the user is watching;
45
+ `ease-out` at 200ms feels faster than `ease-in` at 200ms.
35
46
 
36
- Animate only `transform` and `opacity` — they run on the compositor and don't trigger layout/paint.
37
- Animating `width`, `height`, `top`, `left`, `margin` janks. Use `transform: translate/scale` instead.
47
+ The built-in keywords are weak. Default curves when the project has none:
38
48
 
39
- ## `prefers-reduced-motion` (required)
49
+ ```css
50
+ --ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* UI enter/exit */
51
+ --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* on-screen movement */
52
+ --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* iOS-like drawer/sheet */
53
+ ```
40
54
 
41
- Honor it. Replace movement with an instant or opacity-only change.
55
+ ### Bounce and springs
42
56
 
43
- ```css
44
- @media (prefers-reduced-motion: reduce) {
45
- *, *::before, *::after {
46
- animation-duration: 0.01ms !important;
47
- transition-duration: 0.01ms !important;
48
- }
49
- }
57
+ No bounce, elastic or overshooting `cubic-bezier` (e.g. `cubic-bezier(0.68, -0.55, 0.27, 1.55)`) on UI
58
+ transitions — the `ui-slop-detect` gate flags these as `bounce-easing`.
59
+
60
+ **Springs are allowed only for gesture-driven motion:** drag with momentum, drag-to-dismiss sheets,
61
+ swipe, and anything the user may reverse mid-motion. Springs keep velocity when interrupted, which is what
62
+ a gesture needs. Keep bounce subtle:
63
+
64
+ ```js
65
+ { type: "spring", duration: 0.5, bounce: 0.2 } // bounce 0.1–0.3
50
66
  ```
51
67
 
52
- Don't remove *feedback* entirely — a state change still needs to be visible, just not kinetic.
68
+ A spring on a button, dropdown, modal or toast is out of scope for this exception.
69
+
70
+ ## 3. Duration
71
+
72
+ | Element | Duration |
73
+ |---|---|
74
+ | Button press feedback | 100–160ms |
75
+ | Tooltips, small popovers | 125–200ms |
76
+ | Dropdowns, selects | 150–250ms |
77
+ | Modals, drawers | 200–500ms |
78
+ | Marketing / explanatory | Can be longer |
79
+
80
+ **UI animations stay under 300ms** unless a reason is written down. Under ~80ms is imperceptible: snap
81
+ instead. After the first tooltip in a group opens, the next ones open instantly (no delay, no animation).
82
+
83
+ ## 4. Physicality
84
+
85
+ - **Never `scale(0)`.** Nothing appears from nothing. Start from `scale(0.95)` (0.9–0.97) with `opacity: 0`.
86
+ - **Popovers, dropdowns and tooltips scale from their trigger**, not the center:
87
+ `transform-origin: var(--transform-origin)` (Base UI / Radix expose it). **Modals are exempt** — they
88
+ appear centered and keep `transform-origin: center`.
89
+ - **Press feedback:** `transform: scale(0.97)` on `:active` (0.95–0.98), 100–160ms `ease-out`, on every
90
+ pressable element. It works on touch, unlike hover.
91
+ - **`translateY(100%)`** moves an element by its own height, so drawers and toasts of unknown size need no
92
+ hardcoded pixels.
93
+
94
+ ## 5. Interruptibility and timing
95
+
96
+ - **Transitions over keyframes** for anything triggered rapidly (toasts being added, toggles). A transition
97
+ retargets from its current state; a keyframe animation restarts from zero.
98
+ - Entry without JS: `@starting-style` on the transitioned element.
99
+ - **Asymmetric timing:** slow where the user is deciding (a hold-to-confirm, a destructive press), fast
100
+ where the system responds (the release, the result).
101
+ - **Stagger** group entrances 30–80ms per item, capped so the whole group finishes within ~300ms.
102
+ Stagger entrances, not exits, and never block interaction while it plays.
103
+ - An imperfect crossfade can be bridged with `filter: blur(2px)` during the transition; keep any animated
104
+ blur under 20px.
105
+
106
+ ## 6. Performance
107
+
108
+ Every frame has **16.7ms at 60fps and 8ms at 120Hz** (most phones). Design against 8ms. A fast laptop hides
109
+ almost every problem below; say which findings were verified from code and which need a recording on a
110
+ mid-range device.
111
+
112
+ | Tier | Properties | Cost |
113
+ |---|---|---|
114
+ | Composite only | `transform`, `opacity` (and in current browsers `filter`, `clip-path`) | Cheapest |
115
+ | Paint + composite | `box-shadow`, `border-radius`, `color` | Redraw every frame |
116
+ | Layout + paint + composite | `width`, `height`, `padding`, `margin`, `top`, `left`, `border-width` | Most expensive |
117
+
118
+ - Animate `transform` and `opacity` by default. Swaps: `width`/`height` → `scale()`; `top`/`left`/`margin` →
119
+ `translate()`; animated `box-shadow` → `filter: drop-shadow()`.
120
+ - **Never `transition: all`.** List the exact properties; `all` animates whatever changes next, including
121
+ layout properties nobody meant to animate. (Tailwind: `transition-transform`, `transition-opacity`, not
122
+ `transition-all`.)
123
+ - Do not drive child transforms through a CSS variable set on a parent; it recalculates style for every
124
+ child. Set `transform` on the element itself.
125
+ - In React, a hand-rolled animation writes to `ref.current.style`, never `setState` every frame.
126
+ - Predetermined motion runs in CSS or WAAPI (off the main thread); JS/rAF for dynamic, gesture-driven
127
+ motion. Motion's `x`/`y`/`scale` shorthands are hardware-accelerated from `motion` 14.2; on older versions
128
+ animate the full `transform` string. Layout animations (`layout`, `layoutId`) always run on the main
129
+ thread.
130
+ - `will-change` is the last step, added only after a measured problem (a 1px shift at start/end, dropped
131
+ frames on a recording). Never on `*` or large containers.
132
+ - Name the cause before changing code. "Added `will-change`, seems smoother" is not a diagnosis.
133
+
134
+ ## 7. Accessibility
135
+
136
+ - **`prefers-reduced-motion` means fewer and gentler, not zero.** Drop movement (translate, scale,
137
+ parallax, slide); keep the opacity and color changes that explain a state change.
138
+
139
+ ```css
140
+ @media (prefers-reduced-motion: reduce) {
141
+ .drawer { transition: opacity 200ms ease; transform: none; }
142
+ }
143
+ ```
144
+
145
+ ```jsx
146
+ const reduce = useReducedMotion();
147
+ const closedX = reduce ? 0 : "-100%";
148
+ ```
149
+
150
+ A global `animation-duration: 0.01ms !important` reset is an acceptable project-wide fallback, not the
151
+ design: state changes still have to be visible.
152
+ - **Gate hover motion** behind `@media (hover: hover) and (pointer: fine)`. Touch fires a fake hover on tap
153
+ and leaves it stuck (see `responsive.md`).
154
+
155
+ ## 8. Removing motion
156
+
157
+ When the job is to cut motion (an over-animated product, a performance pass), walk each animation through
158
+ this gate and record the deciding branch:
159
+
160
+ 1. Keyboard-triggered? → **delete**.
161
+ 2. Seen 100+ times a day? → **delete**.
162
+ 3. On functional data? → **delete**, unless it shows a real change in value.
163
+ 4. No nameable purpose? → **keep** if it is a rare first-time moment, otherwise **delete**.
164
+ 5. Purpose, seen tens of times a day? → **reduce**.
165
+ 6. Purpose, rarer: over 300ms, or a slide/scale where a fade carries the same meaning? → **reduce**,
166
+ otherwise **keep**.
167
+
168
+ Marketing pages get more latitude (one hero entrance, explanation), but scroll-reveal on every section
169
+ still fails.
170
+
171
+ | Today | Reduced to |
172
+ |---|---|
173
+ | Hover with scale, lift or shadow growth | Color/opacity only, ≤150ms, `ease` |
174
+ | Slide or scale entrance on a frequent surface | Opacity only, 150ms, `ease-out` |
175
+ | UI animation over 300ms | 200–250ms, same curve |
176
+ | Stagger over 80ms per item or 300ms total | 30ms per item, or fade the group as one |
177
+ | Infinite loop (pulse, shimmer, glow) on a persistent element | Run once, or keep the static end state |
178
+
179
+ Spinners and skeletons on work that resolves under 300ms are deleted too (see `state-matrix.md`).
180
+
181
+ **Removal hazards** — check before deleting, or the removal becomes a regression:
182
+
183
+ - **Hidden start states.** `initial={{ opacity: 0 }}`, a keyframe from `opacity: 0` with
184
+ `fill-mode: forwards`, a reveal class waiting for an observer. Remove the start state with the animation
185
+ or the element stays invisible.
186
+ - **Code waiting on the animation:** `onAnimationEnd`, `transitionend`, `onExitComplete`, `setTimeout`
187
+ sized to a duration.
188
+ - **Exit animations delaying unmount** (`AnimatePresence`): move what ran on exit-complete to the close.
189
+ - **Layout animations masking a jump** (`layout`, FLIP): if the jump appears, keep the animation and report
190
+ the layout shift.
191
+ - **Focus moved in an animation callback** must move immediately.
192
+ - **Tests and selectors** that wait for animation classes or durations.
193
+
194
+ Then delete what the removal orphaned (unused `@keyframes`, `motion.div` with no motion props left, empty
195
+ `AnimatePresence`, unused variants and imports). Propose — never perform — removing a motion library
196
+ dependency.
197
+
198
+ ## 9. Review escalation triggers
199
+
200
+ `/dw-review` treats each of these in a UI diff as a candidate finding (still subject to `dw-review-rigor`
201
+ refutation). Default severity **medium**; **high** when the motion runs on a keyboard-triggered or
202
+ 100+/day action, or when it blocks interaction.
203
+
204
+ - `transition: all` / Tailwind `transition-all`.
205
+ - `scale(0)` as a start state.
206
+ - `ease-in` on any UI enter, exit or interaction.
207
+ - Animation on a keyboard shortcut, command-palette toggle or other 100+/day action.
208
+ - UI duration over 300ms with no stated reason.
209
+ - `transform-origin: center` on a trigger-anchored popover, dropdown or tooltip.
210
+ - Keyframes on toasts, toggles or anything triggered rapidly.
211
+ - Animating `width`, `height`, `margin`, `padding`, `top` or `left` when a transform does the same.
212
+ - A CSS variable on a parent driving child transforms.
213
+ - Movement with no `prefers-reduced-motion` handling.
214
+ - `:hover` motion not gated by `(hover: hover) and (pointer: fine)`.
215
+ - Bounce/overshoot easing, or a spring outside gesture-driven motion.
216
+ - Symmetric timing on a press-and-hold or destructive-confirm interaction.
217
+
218
+ Fix preference, earliest first: delete → reduce → fix easing → fix origin → make interruptible → move to
219
+ composite-only properties → asymmetric timing → polish → accessibility and cohesion.
220
+
221
+ When feel cannot be judged from code (hover feel, perceived speed, gesture physics), say so and ask for a
222
+ slow-motion pass (DevTools animation panel at 2–5× duration) or a real device, instead of guessing.
@@ -1,7 +1,9 @@
1
1
  # Responsive — mobile-first, container queries, fluid space
2
2
 
3
3
  > Patterns adapted from [impeccable](https://github.com/pbakaus/impeccable) (Apache-2.0, derived from
4
- > Anthropic's frontend-design skill). Read when a surface must work across devices.
4
+ > Anthropic's frontend-design skill). The "Mobile platform fixes" section is adapted from the
5
+ > `mobile-native` skill in [`emilkowalski/skills`](https://github.com/emilkowalski/skills)
6
+ > (MIT © 2026 Emil Kowalski). Read when a surface must work across devices.
5
7
 
6
8
  ## Mobile-first
7
9
 
@@ -43,10 +45,50 @@ Keep body text fixed (≥16px); fluid body harms readability.
43
45
  - Touch targets ≥ 24×24 CSS px (≥ 44×44 recommended on mobile). Spacing between targets matters as much
44
46
  as size.
45
47
  - Don't hide primary actions behind hover — hover doesn't exist on touch. Provide a tap-visible path.
46
- - Respect safe-area insets on mobile (`env(safe-area-inset-*)`) for notches/home indicators.
48
+ - Respect safe-area insets on mobile (`env(safe-area-inset-*)`) for notches/home indicators — this needs
49
+ `viewport-fit=cover` in the viewport meta, or every inset is `0px` (see below).
47
50
 
48
51
  ## Layout reflow
49
52
 
50
53
  State explicitly how elements reorganize between breakpoints: stack vs. side-by-side, what collapses into
51
54
  a menu, what hides (and whether hidden content is still reachable). "Hidden on mobile with no alternative"
52
55
  is a bug, not a layout decision.
56
+
57
+ ## Mobile platform fixes
58
+
59
+ Most "the app feels janky on mobile" reports are not animation problems: they are a tap delay, a gray
60
+ flash on tap, or a hover state that will not let go. **None of these reproduce in desktop device
61
+ emulation.** Gate by capability with media queries, never by user-agent sniffing or screen width — touch
62
+ and mouse coexist (iPads with trackpads, touchscreen laptops). Apply each fix where its reason applies, not
63
+ globally out of habit.
64
+
65
+ | Symptom | Cause | Fix |
66
+ |---|---|---|
67
+ | Hover state stuck after tap | Touch fakes `:hover` on the first tap and keeps it | Put `:hover` rules inside `@media (hover: hover) and (pointer: fine)` (Tailwind v4 `hover:` already does; v3 needs `future.hoverOnlyWhenSupported`) and give touch an `:active` state |
68
+ | Gray/blue flash on tap | Browser tap highlight | `html { -webkit-tap-highlight-color: transparent; }`, then make sure every tappable element has its own `:active` feedback |
69
+ | Bottom-pinned UI hidden under the URL bar | `100vh` is the largest viewport | `100dvh` for app shells, drawers and bottom-pinned UI; `100svh` for heroes (stable, no shift on scroll) |
70
+ | Page zooms into an input and stays zoomed | iOS zooms on inputs under 16px | `font-size: 16px` on `input, textarea, select` (scope it with `@media (pointer: coarse)` if desktop needs smaller). **Never** `user-scalable=no` or `maximum-scale=1` — that is an accessibility failure (`accessibility-floor.md` §11) |
71
+ | Tap feels laggy | Double-tap-zoom wait; feedback on release | `touch-action: manipulation` on buttons, links, `[role="button"]`; press feedback on `:active` (100–160ms `ease-out`, see `motion.md`), `pointerdown` not `click` when JS must react |
72
+ | Pull-to-refresh or rubber band hijacks an app | Scroll chaining to the page | `overscroll-behavior: none` on `html, body` in apps (not documents); `contain` on inner scrollers. Never a `touchmove` + `preventDefault()` listener |
73
+ | Content cut by the notch / home indicator | Safe area | `viewport-fit=cover` in the viewport meta + `padding-top: env(safe-area-inset-top)` / `padding-bottom: env(safe-area-inset-bottom, 0px)` on fixed headers, tab bars, toasts and sheets |
74
+ | Long-press selects a button label | Text selection on controls | `user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;` on controls only — never on `body`; users copy addresses and order numbers |
75
+ | Carousel drags the page vertically | Ambiguous gesture axis | `touch-action: pan-y` on a horizontal carousel, `none` only on surfaces that own every axis; prefer native `scroll-snap-type: x mandatory` |
76
+ | Status bar color wrong in one scheme | Single `theme-color` | One `<meta name="theme-color" media="(prefers-color-scheme: …)">` per scheme, matching the top of the page, plus `color-scheme: light dark` |
77
+
78
+ Inputs also set the right keyboard: `type="email"` / `type="tel"`, `inputmode="numeric"` for codes and
79
+ `"decimal"` for amounts, `autocapitalize="none"` on usernames and codes, `enterkeyhint` naming what Return
80
+ does.
81
+
82
+ Baseline for a mobile-facing app:
83
+
84
+ ```html
85
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
86
+ ```
87
+
88
+ `interactive-widget=resizes-content` makes the Android software keyboard shrink the layout viewport, so
89
+ `100dvh` and bottom-pinned inputs react to it the way they do on iOS.
90
+
91
+ **Real hardware is the bar.** Sticky hover, tap delay, URL-bar height, input zoom, overscroll, safe areas
92
+ and the software keyboard only show on a device: dev server on `0.0.0.0`, open it by LAN IP, inspect with
93
+ Safari Web Inspector or `chrome://inspect`. When no device is available, report which fixes were verified
94
+ from code and which still need a phone — never declare them fixed from emulation.