ipad-pro-design
π―Skillfrom matthewharwood/fantasy-phonics
ipad-pro-design skill from matthewharwood/fantasy-phonics
Installation
npx skills add https://github.com/matthewharwood/fantasy-phonics --skill ipad-pro-designSkill Details
CSS and UX patterns for iPad Pro 12.9" development. Covers viewport configuration, touch optimization, safe areas, ProMotion animations, and child-friendly design patterns. Use when building educational apps targeting iPad Pro.
Overview
# iPad Pro 12.9" Design Skill
Comprehensive CSS and UX patterns for building educational web applications optimized for iPad Pro 12.9" (M1/M2/M4). This skill integrates with the project's Utopia fluid scales and web components architecture.
Related Skills
utopia-fluid-scales: Fluid typography and spacing tokens (cqi-based)utopia-container-queries: Container setup for fluid scalesweb-components: Component architecture patternsux-accessibility: Focus management and WCAG complianceux-animation-motion: Animation timing and reduced motionanimejs-v4: Hardware-accelerated animations
---
iPad Pro 12.9" Technical Specifications
Display Specifications
| Property | Value |
|----------|-------|
| Screen Resolution | 2732 x 2048 pixels |
| Points Resolution | 1366 x 1024 points |
| Device Pixel Ratio | 2x (@2x Retina) |
| PPI | 264 pixels per inch |
| Display Type | Liquid Retina XDR |
| Refresh Rate | ProMotion 24-120Hz adaptive |
| Color | P3 wide color gamut |
| Peak Brightness | 1600 nits HDR, 1000 nits full-screen |
Viewport Calculations
```
CSS Pixels (Landscape): 1366 x 1024
CSS Pixels (Portrait): 1024 x 1366
Device Pixels (Landscape): 2732 x 2048
Device Pixels (Portrait): 2048 x 2732
```
Safe Areas
| Edge | Safe Inset (Points) |
|------|---------------------|
| Top (Portrait, no notch) | 24pt status bar |
| Top (Landscape, no notch) | 24pt status bar |
| Bottom (all orientations) | 20pt home indicator |
| Left/Right (Landscape) | 0pt (no notch on 12.9") |
---
Viewport Configuration
Required Meta Tag
```html
```
| Attribute | Purpose |
|-----------|---------|
| width=device-width | Match CSS viewport to device width |
| initial-scale=1 | Prevent default zoom, 1:1 scale |
| viewport-fit=cover | Extend content to screen edges (enables safe area insets) |
AVOID These Viewport Settings
```html
```
PWA Optimizations
```html
```
---
Safe Area Handling
CSS Environment Variables
Safari on iPadOS provides safe area values via env():
```css
:root {
/ Safe area fallbacks for non-Safari browsers /
--safe-area-inset-top: env(safe-area-inset-top, 0px);
--safe-area-inset-right: env(safe-area-inset-right, 0px);
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
--safe-area-inset-left: env(safe-area-inset-left, 0px);
}
```
Applying Safe Areas
```css
/ Full-screen layout with safe areas /
.app-container {
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
}
/ Fixed bottom navigation /
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding-bottom: calc(var(--space-s) + var(--safe-area-inset-bottom));
}
/ Fixed header /
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
padding-top: calc(var(--space-s) + var(--safe-area-inset-top));
}
```
Home Indicator Avoidance
The home indicator appears at the bottom of screen. Avoid placing interactive elements in this zone:
```css
.interactive-bottom-zone {
/ Ensure 20pt clearance from bottom /
margin-bottom: max(20px, var(--safe-area-inset-bottom));
}
```
---
Touch Target Optimization
Apple Human Interface Guidelines
| Guideline | Value | Project Token |
|-----------|-------|---------------|
| Minimum touch target | 44pt x 44pt | --min-touch-target |
| Recommended touch target | 48pt x 48pt | Use --space-xl |
| Child-friendly target | 56pt+ | Use --space-2xl |
| Spacing between targets | 8pt minimum | Use --space-2xs |
Touch Target Patterns
```css
/ Standard touch target (adults) /
.touch-target {
min-width: var(--min-touch-target);
min-height: var(--min-touch-target);
}
/ Large touch target (children 3-8 years) /
.touch-target-child {
min-width: var(--space-2xl); / 72-80px /
min-height: var(--space-2xl);
}
/ Extra-large target (toddlers/accessibility) /
.touch-target-xl {
min-width: var(--space-3xl); / 108-120px /
min-height: var(--space-3xl);
}
/ Expanded hit area for small visual elements /
.touch-expand::before {
content: '';
position: absolute;
inset: -8px;
/ Or for child-friendly: /
inset: calc(-1 * var(--space-s));
}
```
Button Sizing for Children
```css
/ Primary game button - child-friendly /
.game-button {
min-width: var(--space-3xl);
min-height: var(--space-2xl);
padding: var(--space-m) var(--space-xl);
font-size: var(--step-1);
border-radius: var(--space-s);
}
/ Card selection - large tap area /
.word-card {
min-width: 120px;
min-height: 120px;
padding: var(--space-m);
}
```
---
Media Queries for iPad Pro
Device-Specific Targeting
```css
/ iPad Pro 12.9" Landscape /
@media screen and (min-width: 1024px) and (max-width: 1366px) and (orientation: landscape) {
/ Landscape-specific styles /
}
/ iPad Pro 12.9" Portrait /
@media screen and (min-width: 1024px) and (max-width: 1366px) and (orientation: portrait) {
/ Portrait-specific styles /
}
/ iPad Pro 12.9" - Any orientation /
@media screen and (min-width: 1024px) and (max-width: 1366px) {
/ Tablet-specific styles /
}
/ High-DPI screens (Retina) /
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
/ High-DPI assets and adjustments /
}
```
Pointer and Hover Detection
iPadOS supports both touch and Apple Pencil/trackpad. Detect input type:
```css
/ Touch-only devices /
@media (hover: none) and (pointer: coarse) {
.tooltip-trigger:hover .tooltip {
/ Don't show hover tooltips on touch /
display: none;
}
}
/ Devices with fine pointer (trackpad/mouse) /
@media (hover: hover) and (pointer: fine) {
.interactive:hover {
/ Show hover states /
background: var(--color-hover-overlay);
}
}
/ Devices with coarse pointer (finger/stylus) /
@media (pointer: coarse) {
.interactive {
/ Larger touch targets /
min-height: var(--min-touch-target);
min-width: var(--min-touch-target);
}
}
```
Container Queries (Preferred)
Container queries are preferred over viewport queries for component responsiveness:
```css
/ Set container on parent /
.game-board {
container-type: inline-size;
}
/ Component responds to container, not viewport /
@container (inline-size > 800px) {
.word-card {
flex-direction: row;
padding: var(--space-l);
}
}
@container (inline-size <= 600px) {
.word-card {
flex-direction: column;
padding: var(--space-m);
}
}
```
---
ProMotion (120Hz) Animation Optimization
Hardware Acceleration
Safari on iPadOS uses hardware acceleration for specific CSS properties. Always animate these properties for smooth 120Hz performance:
| Property | Hardware Accelerated |
|----------|---------------------|
| transform | Yes - always use |
| opacity | Yes - always use |
| filter | Yes (simple filters) |
| backdrop-filter | Yes |
| will-change | Promotes to GPU layer |
| Property | NOT Hardware Accelerated |
|----------|-------------------------|
| width, height | Triggers layout |
| margin, padding | Triggers layout |
| top, left, right, bottom | Triggers layout |
| border-width | Triggers layout |
| font-size | Triggers layout |
| box-shadow | CPU intensive |
Animation Timing for 120Hz
At 120Hz, each frame is 8.33ms. Animation timing feels different:
```css
:root {
/ Snappy micro-interactions /
--duration-instant: 100ms; / 12 frames at 120Hz /
--duration-quick: 150ms; / 18 frames at 120Hz /
--duration-normal: 200ms; / 24 frames at 120Hz /
--duration-slow: 350ms; / 42 frames at 120Hz /
/ Ease curves for natural feel /
--ease-out: cubic-bezier(0.0, 0.0, 0.2, 1); / Decelerate /
--ease-in-out: cubic-bezier(0.4, 0.0, 0.2, 1); / Standard /
--ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275); / Overshoot /
}
```
CSS Transitions (ProMotion Optimized)
```css
.card {
/ Use transform instead of position /
transform: translateY(0) scale(1);
opacity: 1;
transition:
transform var(--duration-normal) var(--ease-out),
opacity var(--duration-quick) var(--ease-out);
}
.card:active {
/ Press feedback - instant response /
transform: scale(0.97);
transition-duration: var(--duration-instant);
}
.card.selected {
transform: translateY(-8px) scale(1.02);
}
```
Anime.js 4.0 for Complex Animations
Use Anime.js for multi-property animations:
```javascript
import { animate, createSpring } from 'animejs';
// Check reduced motion preference first
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function cardSelectAnimation(element) {
if (prefersReducedMotion) {
element.classList.add('selected');
return;
}
return animate(element, {
scale: [1, 1.05, 1],
translateY: [0, -8, -4],
duration: 250,
ease: 'outBack'
});
}
// Spring physics for natural feel
function dragDropAnimation(element) {
return animate(element, {
x: 0,
y: 0,
ease: createSpring({ stiffness: 400, damping: 25 }),
});
}
```
will-change Usage
Use will-change sparingly - only on elements that will animate:
```css
/ Apply before animation starts /
.card {
will-change: transform;
}
/ Remove after animation completes (via JavaScript) /
.card.animation-complete {
will-change: auto;
}
```
```javascript
// Anime.js lifecycle hooks
animate(element, {
scale: [1, 1.1, 1],
onBegin: () => {
element.style.willChange = 'transform';
},
onComplete: () => {
element.style.willChange = 'auto';
}
});
```
---
Touch Interaction Patterns
Pointer Events (Unified API)
Use Pointer Events API for unified touch/mouse/stylus handling:
```javascript
class DraggableCard extends HTMLElement {
#isDragging = false;
#startX = 0;
#startY = 0;
connectedCallback() {
this.addEventListener('pointerdown', this);
this.addEventListener('pointermove', this);
this.addEventListener('pointerup', this);
this.addEventListener('pointercancel', this);
// Prevent default touch behaviors
this.style.touchAction = 'none';
}
disconnectedCallback() {
this.removeEventListener('pointerdown', this);
this.removeEventListener('pointermove', this);
this.removeEventListener('pointerup', this);
this.removeEventListener('pointercancel', this);
}
handleEvent(e) {
switch (e.type) {
case 'pointerdown':
this.#handlePointerDown(e);
break;
case 'pointermove':
this.#handlePointerMove(e);
break;
case 'pointerup':
case 'pointercancel':
this.#handlePointerUp(e);
break;
}
}
#handlePointerDown(e) {
this.#isDragging = true;
this.#startX = e.clientX;
this.#startY = e.clientY;
// Capture pointer for reliable tracking
this.setPointerCapture(e.pointerId);
// Visual feedback
this.setAttribute('aria-grabbed', 'true');
}
#handlePointerMove(e) {
if (!this.#isDragging) return;
const deltaX = e.clientX - this.#startX;
const deltaY = e.clientY - this.#startY;
// Direct transform for 120Hz smoothness - no animate() during drag
this.style.transform = translate(${deltaX}px, ${deltaY}px);
}
#handlePointerUp(e) {
if (!this.#isDragging) return;
this.#isDragging = false;
this.releasePointerCapture(e.pointerId);
this.removeAttribute('aria-grabbed');
// Animate back with physics
this.#animateDrop();
}
#animateDrop() {
animate(this, {
x: 0,
y: 0,
duration: 300,
ease: 'outBack'
});
}
}
```
CSS touch-action Control
```css
/ Allow vertical scroll only /
.scrollable-list {
touch-action: pan-y;
}
/ Disable all touch behaviors (for custom drag) /
.draggable {
touch-action: none;
}
/ Allow pinch-zoom but not pan /
.zoomable-image {
touch-action: pinch-zoom;
}
/ Default - allow all /
.interactive {
touch-action: manipulation; / Disables double-tap zoom delay /
}
```
Tap vs Long-Press
```javascript
class TappableElement extends HTMLElement {
#longPressTimeout = null;
#longPressTriggered = false;
static LONG_PRESS_DURATION = 500; // ms
connectedCallback() {
this.addEventListener('pointerdown', this);
this.addEventListener('pointerup', this);
this.addEventListener('pointercancel', this);
}
handleEvent(e) {
switch (e.type) {
case 'pointerdown':
this.#longPressTriggered = false;
this.#longPressTimeout = setTimeout(() => {
this.#longPressTriggered = true;
this.#handleLongPress(e);
}, TappableElement.LONG_PRESS_DURATION);
break;
case 'pointerup':
clearTimeout(this.#longPressTimeout);
if (!this.#longPressTriggered) {
this.#handleTap(e);
}
break;
case 'pointercancel':
clearTimeout(this.#longPressTimeout);
break;
}
}
#handleTap(e) {
this.dispatchEvent(new CustomEvent('tap', {
bubbles: true,
detail: { x: e.clientX, y: e.clientY }
}));
}
#handleLongPress(e) {
// Haptic feedback if available
if (navigator.vibrate) navigator.vibrate(10);
this.dispatchEvent(new CustomEvent('longpress', {
bubbles: true,
detail: { x: e.clientX, y: e.clientY }
}));
}
}
```
Hover State Handling
iPadOS triggers :hover on first tap, then :active on second. Handle this:
```css
/ Default: no hover effects for touch /
@media (hover: none) {
.button:hover {
/ Reset hover styles for touch devices /
background: var(--button-bg);
}
}
/ Only apply hover for devices that support it /
@media (hover: hover) {
.button:hover {
background: var(--color-hover-overlay);
transform: translateY(-1px);
}
}
/ Active state works on both /
.button:active {
background: var(--color-active-overlay);
transform: scale(0.98);
}
```
---
Layout Patterns for 12.9" Display
Grid for Large Tablets
```css
/ Game board - uses full width /
.game-board {
container-type: inline-size;
display: grid;
gap: var(--grid-gutter);
padding: var(--space-m);
}
/ Landscape: 4 columns /
@container (inline-size >= 1024px) {
.card-grid {
grid-template-columns: repeat(4, 1fr);
}
}
/ Portrait: 3 columns /
@container (inline-size >= 768px) and (inline-size < 1024px) {
.card-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/ Small containers: 2 columns /
@container (inline-size < 768px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
```
Avoiding "Blown-Up Phone" Layouts
```css
/ DON'T: Single column on large screens /
.page-content {
max-width: 100%; / Content stretches uncomfortably /
}
/ DO: Constrain content width, use whitespace /
.page-content {
max-width: var(--grid-max-width); / 1240px /
margin-inline: auto;
padding-inline: var(--space-l);
}
/ DO: Multi-column layouts on large screens /
.main-layout {
display: grid;
grid-template-columns: 1fr 2fr 1fr; / Sidebar | Main | Aside /
gap: var(--grid-gutter);
}
```
Orientation Change Handling
```css
/ Landscape-specific adjustments /
@media (orientation: landscape) {
.game-ui {
flex-direction: row;
.sidebar {
width: 280px;
flex-shrink: 0;
}
.main-content {
flex: 1;
}
}
}
/ Portrait-specific adjustments /
@media (orientation: portrait) {
.game-ui {
flex-direction: column;
.sidebar {
width: 100%;
height: auto;
}
.main-content {
flex: 1;
}
}
}
```
JavaScript Orientation Detection
```javascript
class OrientationAware extends HTMLElement {
#mediaQuery = window.matchMedia('(orientation: landscape)');
connectedCallback() {
this.#mediaQuery.addEventListener('change', this);
this.#updateOrientation();
}
disconnectedCallback() {
this.#mediaQuery.removeEventListener('change', this);
}
handleEvent(e) {
if (e.type === 'change') {
this.#updateOrientation();
}
}
#updateOrientation() {
const isLandscape = this.#mediaQuery.matches;
this.setAttribute('data-orientation', isLandscape ? 'landscape' : 'portrait');
this.dispatchEvent(new CustomEvent('orientation-change', {
bubbles: true,
detail: { isLandscape }
}));
}
}
```
---
Child-Friendly UX Patterns
Design Principles for Children (Ages 3-8)
| Principle | Implementation |
|-----------|----------------|
| Large tap targets | Minimum 56pt (prefer 72pt+) |
| Clear visual feedback | Immediate bounce/glow on tap |
| Simple navigation | Max 2-3 choices visible |
| Forgiving interactions | Accept imprecise taps, allow undo |
| Consistent layout | Same button positions across screens |
| No time pressure | Avoid timers for young children |
| Positive reinforcement | Celebrate success, gentle error handling |
Visual Feedback for Children
```javascript
// Immediate, exaggerated feedback
function childFriendlyTapFeedback(element) {
// Instant scale down
animate(element, {
scale: 0.9,
duration: 80,
ease: 'linear'
}).then(() => {
// Bouncy return
animate(element, {
scale: [0.9, 1.15, 1],
duration: 300,
ease: 'outBack'
});
});
}
// Success celebration
function celebrateSuccess(element) {
return animate(element, {
scale: [1, 1.3, 1],
rotate: [0, -8, 8, -4, 4, 0],
duration: 500,
ease: 'outElastic(1, 0.5)'
});
}
// Gentle error shake (not scary)
function gentleErrorShake(element) {
return animate(element, {
x: [0, -4, 4, -2, 2, 0],
duration: 300,
ease: 'linear'
});
}
```
Progressive Disclosure
```html
play_arrow
Play
settings
Settings
```
Forgiving Touch Zones
```css
/ Extra-large interactive cards for children /
.word-card-child {
min-width: 140px;
min-height: 140px;
padding: var(--space-l);
/ Expanded hit area beyond visual bounds /
position: relative;
}
.word-card-child::before {
content: '';
position: absolute;
inset: calc(-1 var(--space-m)); / 27-30px expansion */
}
/ Wide spacing between cards to prevent mis-taps /
.card-grid-child {
gap: var(--space-l); / 36-40px /
}
```
Audio Feedback (Optional)
```javascript
class AudioFeedback {
#audioContext = null;
#enabled = true;
constructor() {
// Initialize on first user interaction
document.addEventListener('pointerdown', () => {
if (!this.#audioContext) {
this.#audioContext = new AudioContext();
}
}, { once: true });
}
playTap() {
if (!this.#enabled || !this.#audioContext) return;
this.#playTone(800, 50); // Short, high beep
}
playSuccess() {
if (!this.#enabled || !this.#audioContext) return;
this.#playMelody([523, 659, 784], 100); // C-E-G chord
}
playError() {
if (!this.#enabled || !this.#audioContext) return;
this.#playTone(200, 150); // Low, longer tone
}
#playTone(frequency, duration) {
const oscillator = this.#audioContext.createOscillator();
const gainNode = this.#audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(this.#audioContext.destination);
oscillator.frequency.value = frequency;
gainNode.gain.setValueAtTime(0.1, this.#audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.#audioContext.currentTime + duration / 1000);
oscillator.start();
oscillator.stop(this.#audioContext.currentTime + duration / 1000);
}
#playMelody(frequencies, noteDuration) {
frequencies.forEach((freq, i) => {
setTimeout(() => this.#playTone(freq, noteDuration), i * noteDuration);
});
}
toggle(enabled) {
this.#enabled = enabled;
}
}
```
---
Safari/WebKit Considerations
Supported CSS Features (iPadOS 17+)
| Feature | Support | Notes |
|---------|---------|-------|
| Container Queries | Full | Use cqi units |
| :has() selector | Full | Use for complex states |
| Subgrid | Full | Use for nested grids |
| view-transitions | Partial | Same-document only |
| field-sizing | Yes | Auto-sizing textareas |
| @layer | Full | CSS cascade layers |
| color-mix() | Full | Dynamic color blending |
| Backdrop filter | Full | With -webkit- prefix |
Safari-Specific CSS
```css
/ Backdrop blur - requires prefix /
.modal-backdrop {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
/ Prevent iOS font boosting /
body {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
/ Smooth scrolling with momentum /
.scrollable {
-webkit-overflow-scrolling: touch;
overflow-y: auto;
}
/ Hide scrollbar but keep functionality /
.scrollable::-webkit-scrollbar {
display: none;
}
```
Known Safari Limitations
| Issue | Workaround |
|-------|------------|
| No popover attribute | Use or custom implementation |
| Limited Fullscreen API | Works in standalone PWA mode only |
| No Web MIDI | Use Web Audio API instead |
| IndexedDB storage limits | 1GB quota, request via Storage API |
| No beforeinstallprompt | Manual PWA install instructions |
PWA Capabilities
```json
// manifest.json
{
"name": "Fantasy Phonics",
"short_name": "Phonics",
"display": "standalone",
"orientation": "any",
"background_color": "#252119",
"theme_color": "#252119",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
],
"start_url": "/",
"scope": "/"
}
```
---
Performance Checklist
Rendering Performance
- [ ] Use
transformandopacityfor animations (GPU accelerated) - [ ] Apply
will-changebefore animations, remove after - [ ] Use
contain: layouton fixed-size containers - [ ] Avoid animating during scroll (use
scroll-behavior: smoothinstead) - [ ] Batch DOM reads and writes (avoid layout thrashing)
Touch Performance
- [ ] Use
touch-action: manipulationto remove 300ms tap delay - [ ] Apply
pointer-events: noneto elements during drag - [ ] Use
pointerdown/pointermove/pointerup(not touch events) - [ ] Set
passive: trueon scroll/touch listeners when not preventing default
Memory Management
- [ ] Clean up event listeners in
disconnectedCallback - [ ] Cancel animations when component unmounts
- [ ] Use
AbortControllerfor cancellable fetch requests - [ ] Avoid creating objects in animation loops
Asset Optimization
- [ ] Use
srcsetfor images at 1x and 2x resolutions - [ ] Lazy load images below the fold with
loading="lazy" - [ ] Preload critical fonts with
- [ ] Use WebP/AVIF images where supported
---
Quick Reference
Viewport & Safe Areas
```css
/ Essential meta tag /
/ Safe area padding /
padding: env(safe-area-inset-top) env(safe-area-inset-right)
env(safe-area-inset-bottom) env(safe-area-inset-left);
```
Touch Targets
```css
/ Adults / min-width: 44px; min-height: 44px;
/ Children / min-width: 72px; min-height: 72px;
```
Media Queries
```css
/ iPad Pro 12.9" specific /
@media (min-width: 1024px) and (max-width: 1366px) { }
/ Touch-only /
@media (hover: none) and (pointer: coarse) { }
/ Orientation /
@media (orientation: landscape) { }
```
Animation Timing (120Hz)
```css
--duration-instant: 100ms; / Tap feedback /
--duration-quick: 150ms; / State changes /
--duration-normal: 200ms; / Transitions /
```
Integration with Project Tokens
```css
/ Use Utopia space tokens for consistent sizing /
.touch-target-child {
min-width: var(--space-2xl); / 72-80px /
min-height: var(--space-2xl);
padding: var(--space-m); / 27-30px /
gap: var(--space-s); / 18-20px /
}
/ Use Utopia type tokens for readable text /
.game-label {
font-size: var(--step-1); / 21.6-25px /
}
```
---
Files
This skill references and integrates with:
css/styles/accessibility.css- Touch target tokens, focus ringscss/styles/space.css- Utopia spacing scalecss/styles/typography.css- Utopia type scalecss/styles/transitions.css- Animation timing tokens
More from this repository10
ux-design-principles skill from matthewharwood/fantasy-phonics
web-audio skill from matthewharwood/fantasy-phonics
audio-design skill from matthewharwood/fantasy-phonics
animejs-v4 skill from matthewharwood/fantasy-phonics
utopia-fluid-scales skill from matthewharwood/fantasy-phonics
ux-user-flow skill from matthewharwood/fantasy-phonics
ux-spacing-layout skill from matthewharwood/fantasy-phonics
material-symbols-v3 skill from matthewharwood/fantasy-phonics
idb-state-persistence skill from matthewharwood/fantasy-phonics
ux-animation-motion skill from matthewharwood/fantasy-phonics