iOS adaptive dashboard: size classes, readable guide & priorities
A Health-style summary built like a UIKit screen, with widget cards that go from one column to two by swapping one data-⚓️-side value and their width multipliers.
<div class="stage" data-device="fill" data-orient="portrait">
<div class="device">
<div class="app" data-⚓️-root data-size-class="compact" data-booting>
<!-- the whole screen as a layout guide (the device notch/home indicator anchor to it) -->
<div class="screen-guide" data-⚓️-name="--screen" aria-hidden="true"></div>
<!-- regular width only: iPadOS-style sidebar (primary column of a split view) -->
<nav class="sidebar" aria-label="Health sections">
<h2 class="sidebar-title">Health</h2>
<label class="search"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="8.5" cy="8.5" r="5.5"/><path d="m13 13 4 4"/></svg><input type="search" placeholder="Search" aria-label="Search"></label>
<ul class="side-list">
<li><button class="side-item is-current" data-go="top"><span class="side-icon" style="--tint: var(--pink)">♥</span>Summary</button></li>
<li><button class="side-item" data-go="top"><span class="side-icon" style="--tint: var(--blue)">⇄</span>Sharing</button></li>
</ul>
<h3 class="side-heading">Health Categories</h3>
<ul class="side-list">
<li><button class="side-item" data-go="c1"><span class="side-icon" style="--tint: var(--red)">◎</span>Activity</button></li>
<li><button class="side-item" data-go="c3"><span class="side-icon" style="--tint: var(--pink)">♥</span>Heart</button></li>
<li><button class="side-item" data-go="c6"><span class="side-icon" style="--tint: var(--teal)">✺</span>Mindfulness</button></li>
<li><button class="side-item" data-go="c5"><span class="side-icon" style="--tint: var(--orange)">➚</span>Mobility</button></li>
<li><button class="side-item" data-go="c7"><span class="side-icon" style="--tint: var(--green)">✚</span>Nutrition</button></li>
<li><button class="side-item" data-go="c4"><span class="side-icon" style="--tint: var(--indigo)">☾</span>Sleep</button></li>
</ul>
</nav>
<section class="pane" data-⚓️-root data-⚓️-name="--pane" aria-label="Summary">
<!-- safeAreaLayoutGuide: the pane minus the (real or simulated) safe-area insets -->
<div class="safe-guide" data-⚓️-name="--safe" data-ix="safeAreaLayoutGuide" aria-hidden="true"></div>
<div class="scroller">
<div class="content" data-⚓️-root>
<!-- layoutMarginsGuide = safe area inset by 16pt (compact) / 20pt (regular) -->
<div class="margins-guide" data-⚓️-name="--margins" data-ix="layoutMarginsGuide">
<!-- readableContentGuide = margins, capped at 672pt wide and centred -->
<div class="readable-guide" data-⚓️-name="--readable" data-ix="readableContentGuide">
<header class="large-header" data-⚓️-name="--header" data-ix="header">
<p class="eyebrow">Thursday, 24 September</p>
<h1 class="large-title" data-⚓️-name="--large-title" data-ix="largeTitle">Summary</h1>
</header>
</div>
</div>
<!-- avatar: centerY to the large title, trailing to the readable guide -->
<button class="avatar" aria-label="Profile" data-ix="avatar"
data-⚓️-to="--large-title" data-⚓️-align-y="center" data-⚓️-align-trailing="--readable">AA</button>
<!-- Cards: each one is anchored to the previous one (a hand-built stack view).
JS only swaps the side (below | after) when the size class changes. -->
<article class="card" id="c1" style="--tint: var(--red)" data-ix="activityCard" data-regular="below"
data-⚓️-name="--c1" data-⚓️-to="--header" data-⚓️-side="below"
data-⚓️-align-leading="--readable" data-⚓️-sync-width="--readable" data-⚓️-transition="glide">
<div class="card-body" data-⚓️-scope="all">
<div class="card-head" data-⚓️-name="--head">
<span class="card-title" data-⚓️-name="--title"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 2c1 3 5 5 5 10a5 5 0 0 1-10 0c0-3 2-4 2-6 2 1 2 3 2 3 1-2 1-5 1-7Z"/></svg>Activity</span>
</div>
<span class="card-meta" data-⚓️-to="--head" data-⚓️-after="--title" data-⚓️-align-trailing="--head" data-⚓️-align-y="center"><span>Today, 9:41 AM</span></span>
<div class="activity">
<svg class="rings" viewBox="0 0 100 100" data-ix="rings" aria-label="Move 84%, Exercise 80%, Stand 75%" role="img">
<circle class="track m" cx="50" cy="50" r="42"/><circle class="ring m" cx="50" cy="50" r="42" pathLength="100" stroke-dasharray="84 100"/>
<circle class="track e" cx="50" cy="50" r="30"/><circle class="ring e" cx="50" cy="50" r="30" pathLength="100" stroke-dasharray="80 100"/>
<circle class="track s" cx="50" cy="50" r="18"/><circle class="ring s" cx="50" cy="50" r="18" pathLength="100" stroke-dasharray="75 100"/>
</svg>
<dl class="stats">
<div style="--tint: var(--move)"><dt>Move</dt><dd>420<small>/500 cal</small></dd></div>
<div style="--tint: var(--exercise)"><dt>Exercise</dt><dd>24<small>/30 min</small></dd></div>
<div style="--tint: var(--stand)"><dt>Stand</dt><dd>9<small>/12 hr</small></dd></div>
</dl>
</div>
</div>
<button class="card-hit" data-⚓️-pin-all aria-label="Open Activity details"></button>
</article>
<article class="card" id="c2" style="--tint: var(--orange)" data-ix="stepsCard" data-span="half" data-regular="below"
data-⚓️-name="--c2" data-⚓️-to="--c1" data-⚓️-side="below"
data-⚓️-align-leading="--readable" data-⚓️-sync-width="--readable" data-⚓️-transition="glide">
<div class="card-body" data-⚓️-scope="all">
<div class="card-head" data-⚓️-name="--head">
<span class="card-title" data-⚓️-name="--title" data-ix="steps.title" data-ix-extra="compressionResistance = 750 (required-ish: never truncates first)"><svg viewBox="0 0 20 20" aria-hidden="true"><ellipse cx="7" cy="7" rx="3" ry="4.5"/><ellipse cx="13" cy="12" rx="3" ry="4.5"/></svg>Steps</span>
</div>
<span class="card-meta" data-ix="steps.time" data-ix-extra="contentHugging = 750, compressionResistance = 250 (truncates first)"
data-⚓️-to="--head" data-⚓️-after="--title" data-⚓️-align-trailing="--head" data-⚓️-align-y="center"><span>Today, 9:41 AM</span></span>
<p class="metric" data-⚓️-name="--num"><span class="num">8,432</span> <span class="unit">steps</span></p>
<span class="badge" data-ix="steps.trend" data-ix-priority="250" data-ix-extra="optional: removed when it can't fit (no-overflow)"
data-⚓️-to="--num" data-⚓️-side="after" data-⚓️-align-y="center" data-⚓️-visibility="no-overflow">▲ 12%</span>
<div class="bars" aria-hidden="true"><i style="--v:.42"></i><i style="--v:.66"></i><i style="--v:.5"></i><i style="--v:.9"></i><i style="--v:.72"></i><i style="--v:.38"></i><i style="--v:.8"></i></div>
</div>
<button class="card-hit" data-⚓️-pin-all aria-label="Open Steps details"></button>
</article>
<article class="card" id="c3" style="--tint: var(--pink)" data-ix="heartCard" data-span="half" data-regular="after"
data-⚓️-name="--c3" data-⚓️-to="--c2" data-⚓️-side="below"
data-⚓️-align-leading="--readable" data-⚓️-sync-width="--readable" data-⚓️-transition="glide">
<div class="card-body" data-⚓️-scope="all">
<div class="card-head" data-⚓️-name="--head">
<span class="card-title" data-⚓️-name="--title"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 17S3 12.5 3 7.5A3.5 3.5 0 0 1 10 6a3.5 3.5 0 0 1 7 1.5C17 12.5 10 17 10 17Z"/></svg>Heart Rate</span>
</div>
<span class="card-meta" data-⚓️-to="--head" data-⚓️-after="--title" data-⚓️-align-trailing="--head" data-⚓️-align-y="center"><span>9:12 AM</span></span>
<p class="metric" data-⚓️-name="--num"><span class="num">68</span> <span class="unit">BPM</span></p>
<span class="badge" data-⚓️-to="--num" data-⚓️-side="after" data-⚓️-align-y="center" data-⚓️-visibility="no-overflow">Resting 58</span>
<div class="ranges" aria-hidden="true"><i style="--a:.35;--b:.7"></i><i style="--a:.3;--b:.55"></i><i style="--a:.4;--b:.95"></i><i style="--a:.25;--b:.6"></i><i style="--a:.3;--b:.8"></i><i style="--a:.45;--b:.7"></i><i style="--a:.2;--b:.5"></i><i style="--a:.35;--b:.85"></i><i style="--a:.3;--b:.62"></i></div>
</div>
<button class="card-hit" data-⚓️-pin-all aria-label="Open Heart Rate details"></button>
</article>
<article class="card" id="c4" style="--tint: var(--indigo)" data-ix="sleepCard" data-span="half" data-regular="below"
data-⚓️-name="--c4" data-⚓️-to="--c3" data-⚓️-side="below"
data-⚓️-align-leading="--readable" data-⚓️-sync-width="--readable" data-⚓️-transition="glide">
<div class="card-body" data-⚓️-scope="all">
<div class="card-head" data-⚓️-name="--head">
<span class="card-title" data-⚓️-name="--title"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M13 3a7 7 0 1 0 4 11A6 6 0 0 1 13 3Z"/></svg>Sleep</span>
</div>
<span class="card-meta" data-⚓️-to="--head" data-⚓️-after="--title" data-⚓️-align-trailing="--head" data-⚓️-align-y="center"><span>Last night</span></span>
<p class="metric" data-⚓️-name="--num"><span class="num">7</span> <span class="unit">hr</span> <span class="num">21</span> <span class="unit">min</span></p>
<div class="stages" aria-hidden="true"><i style="--x:0;--w:.12;--y:1"></i><i style="--x:.12;--w:.2;--y:2"></i><i style="--x:.32;--w:.1;--y:3"></i><i style="--x:.42;--w:.18;--y:2"></i><i style="--x:.6;--w:.08;--y:0"></i><i style="--x:.68;--w:.14;--y:2"></i><i style="--x:.82;--w:.1;--y:3"></i><i style="--x:.92;--w:.08;--y:1"></i></div>
</div>
<button class="card-hit" data-⚓️-pin-all aria-label="Open Sleep details"></button>
</article>
<article class="card" id="c5" style="--tint: var(--orange)" data-ix="distanceCard" data-span="half" data-regular="after"
data-⚓️-name="--c5" data-⚓️-to="--c4" data-⚓️-side="below"
data-⚓️-align-leading="--readable" data-⚓️-sync-width="--readable" data-⚓️-transition="glide">
<div class="card-body" data-⚓️-scope="all">
<div class="card-head" data-⚓️-name="--head">
<span class="card-title" data-⚓️-name="--title" data-ix="distance.title" data-ix-extra="compressionResistance = 750"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 15 8 9l3 3 6-7M13 5h4v4"/></svg>Walking + Running Distance</span>
</div>
<span class="card-meta" data-ix="distance.time" data-ix-extra="contentHugging = 750, compressionResistance = 250 (truncates first)"
data-⚓️-to="--head" data-⚓️-after="--title" data-⚓️-align-trailing="--head" data-⚓️-align-y="center"><span>Yesterday, 11:58 PM</span></span>
<p class="metric" data-⚓️-name="--num"><span class="num">5.2</span> <span class="unit">km</span></p>
<span class="badge" data-ix="distance.trend" data-ix-priority="250" data-ix-extra="optional: removed when it can't fit (no-overflow)"
data-⚓️-to="--num" data-⚓️-side="after" data-⚓️-align-y="center" data-⚓️-visibility="no-overflow">▲ 8% vs avg</span>
<svg class="line" viewBox="0 0 140 56" preserveAspectRatio="none" aria-hidden="true"><polyline points="0,44 20,38 40,40 60,26 80,30 100,14 120,20 140,10"/></svg>
</div>
<button class="card-hit" data-⚓️-pin-all aria-label="Open Walking and Running Distance details"></button>
</article>
<article class="card" id="c6" style="--tint: var(--teal)" data-ix="mindfulCard" data-span="half" data-regular="below"
data-⚓️-name="--c6" data-⚓️-to="--c5" data-⚓️-side="below"
data-⚓️-align-leading="--readable" data-⚓️-sync-width="--readable" data-⚓️-transition="glide">
<div class="card-body" data-⚓️-scope="all">
<div class="card-head" data-⚓️-name="--head">
<span class="card-title" data-⚓️-name="--title"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="3"/><circle cx="10" cy="4.5" r="2.5"/><circle cx="10" cy="15.5" r="2.5"/><circle cx="4.5" cy="10" r="2.5"/><circle cx="15.5" cy="10" r="2.5"/></svg>Mindful Minutes</span>
</div>
<span class="card-meta" data-⚓️-to="--head" data-⚓️-after="--title" data-⚓️-align-trailing="--head" data-⚓️-align-y="center"><span>Today</span></span>
<p class="metric" data-⚓️-name="--num"><span class="num">15</span> <span class="unit">min</span></p>
<div class="dots" aria-hidden="true"><i></i><i></i><i class="off"></i><i></i><i></i><i class="off"></i><i></i></div>
</div>
<button class="card-hit" data-⚓️-pin-all aria-label="Open Mindful Minutes details"></button>
</article>
<article class="card" id="c7" style="--tint: var(--green)" data-ix="waterCard" data-span="half" data-regular="after"
data-⚓️-name="--c7" data-⚓️-to="--c6" data-⚓️-side="below"
data-⚓️-align-leading="--readable" data-⚓️-sync-width="--readable" data-⚓️-transition="glide">
<div class="card-body" data-⚓️-scope="all">
<div class="card-head" data-⚓️-name="--head">
<span class="card-title" data-⚓️-name="--title"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 2s5 6 5 10a5 5 0 0 1-10 0c0-4 5-10 5-10Z"/></svg>Water</span>
</div>
<span class="card-meta" data-⚓️-to="--head" data-⚓️-after="--title" data-⚓️-align-trailing="--head" data-⚓️-align-y="center"><span>8:30 AM</span></span>
<p class="metric" data-⚓️-name="--num"><span class="num">1.4</span> <span class="unit">L</span></p>
<span class="badge" data-⚓️-to="--num" data-⚓️-side="after" data-⚓️-align-y="center" data-⚓️-visibility="no-overflow">56% of goal</span>
<div class="glass" aria-hidden="true"><i style="--v:.56"></i></div>
</div>
<button class="card-hit" data-⚓️-pin-all aria-label="Open Water details"></button>
</article>
<!-- contentInset.bottom: clearance for the floating toolbar, chained below the last card -->
<div class="bottom-inset" aria-hidden="true" data-⚓️-to="--c7" data-⚓️-side="below" data-⚓️-align-leading="--readable"></div>
</div>
</div>
<!-- blur navigation bar: pinned to the pane's top, inline title centred on the safe area -->
<div class="navbar" data-⚓️-pin-top data-⚓️-pin-x aria-hidden="true"></div>
<p class="inline-title" aria-hidden="true" data-ix="inlineTitle"
data-⚓️-to="--safe" data-⚓️-align-top data-⚓️-align-x="center">Summary</p>
<!-- X-ray bands: full-height stripes stretched to the two guides living in the scroller -->
<div class="xr-band xr-margins" aria-hidden="true" data-⚓️-stretch-x="--margins" data-⚓️-pin-y></div>
<div class="xr-band xr-readable" aria-hidden="true" data-⚓️-stretch-x="--readable" data-⚓️-pin-y></div>
<!-- floating toolbar: bottom = safeArea.bottom - 8, centerX = safeArea.centerX -->
<div class="pill" role="toolbar" aria-label="Demo controls" data-ix="toolbar"
data-⚓️-name="--pill" data-⚓️-to="--safe" data-⚓️-align-bottom data-⚓️-align-x="center">
<button class="pill-btn" id="inspect" aria-pressed="false"><svg viewBox="0 0 20 20" aria-hidden="true"><rect x="3" y="3" width="14" height="14" rx="3"/><path d="M3 8h14M8 8v9"/></svg><span>Constraints</span></button>
<button class="pill-btn" id="sizeclass" aria-label="Size class"><svg viewBox="0 0 20 20" aria-hidden="true"><rect x="2.5" y="5" width="6" height="10" rx="1.5"/><rect x="11" y="3.5" width="6.5" height="13" rx="1.5"/></svg><span>Auto</span></button>
<button class="pill-btn" id="rotate" aria-label="Device"><svg viewBox="0 0 20 20" aria-hidden="true"><rect x="6" y="2.5" width="8" height="15" rx="2"/><path d="M2.5 9a7.5 7.5 0 0 1 3-5.5M17.5 11a7.5 7.5 0 0 1-3 5.5"/></svg><span>Fill</span></button>
<button class="pill-btn" id="dt" aria-label="Dynamic Type size"><span class="aa">Aa</span><span class="dt-label">Default</span></button>
</div>
</section>
<!-- simulated hardware (device-frame modes only) -->
<div class="island" aria-hidden="true" data-⚓️-name="--island" data-⚓️-to="--screen" data-⚓️-align-top data-⚓️-align-x="center"></div>
<span class="status-time" aria-hidden="true" data-⚓️-to="--island" data-⚓️-align-y="center" data-⚓️-align-leading="--screen">9:41</span>
<span class="status-icons" aria-hidden="true" data-⚓️-to="--island" data-⚓️-align-y="center" data-⚓️-align-trailing="--screen"><i></i><i></i><b></b></span>
<div class="home-indicator" aria-hidden="true" data-⚓️-to="--screen" data-⚓️-align-bottom data-⚓️-align-x="center"></div>
</div>
</div>
<p class="caption" aria-live="polite"></p>
</div>
<!-- Constraints inspector: grows out of the toolbar and sits above it -->
<div class="inspector" id="inspector" popover="manual" aria-label="Constraints"
data-⚓️-to="--pill" data-⚓️-side="above" data-⚓️-align-x="center"
data-⚓️-morph-from data-⚓️-transition="morph">
<header class="inspector-head">
<strong>Constraints</strong>
<span class="traits"></span>
</header>
<ol class="ix-list"></ol>
<p class="ix-foot">Generated from the live <code>data-⚓️-*</code> attributes. Hover or focus a row to highlight its view.</p>
</div>
<!-- Detail sheet: compact = bottom sheet, regular = centred form sheet; morphs out of the tapped card -->
<dialog class="sheet" aria-labelledby="sheet-title" data-⚓️-to="--pane" data-⚓️-transition="morph">
<div class="grabber" aria-hidden="true"></div>
<header class="sheet-bar">
<h2 id="sheet-title">Steps</h2>
<form method="dialog"><button class="done">Done</button></form>
</header>
<div class="sheet-content"></div>
</dialog>/* Demo styling only. Every view's position comes from data-⚓️-* attributes;
this file sets looks, sizes and the three layout guides' own boxes
(safe area, layout margins, readable width), exactly the things UIKit
hands you rather than things you constrain. */
:root {
color-scheme: light dark;
--red: light-dark(#ff3b30, #ff453a);
--orange: light-dark(#ff9500, #ff9f0a);
--pink: light-dark(#ff2d55, #ff375f);
--indigo: light-dark(#5856d6, #5e5ce6);
--teal: light-dark(#30b0c7, #40c8e0);
--green: light-dark(#34c759, #30d158);
--blue: light-dark(#007aff, #0a84ff);
--move: #fa114f;
--exercise: light-dark(#76d016, #a6ff00);
--stand: light-dark(#00c7d6, #00fff6);
--bg: light-dark(#f2f2f7, #000);
--card: light-dark(#fff, #1c1c1e);
--card-2: light-dark(#fff, #2c2c2e);
--label: light-dark(#000, #fff);
--label-2: light-dark(rgb(60 60 67 / 0.6), rgb(235 235 245 / 0.6));
--label-3: light-dark(rgb(60 60 67 / 0.3), rgb(235 235 245 / 0.3));
--sep: light-dark(rgb(60 60 67 / 0.29), rgb(84 84 88 / 0.65));
--fill: light-dark(rgb(120 120 128 / 0.12), rgb(118 118 128 / 0.24));
--glass: light-dark(rgb(255 255 255 / 0.72), rgb(44 44 46 / 0.7));
--dt-scale: 1;
--space: 8px; /* UIKit "standard spacing" */
--ease: cubic-bezier(0.25, 1, 0.5, 1);
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font: 400 17px/1.3 system-ui, -apple-system, "SF Pro Text", "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
background: var(--bg);
color: var(--label);
}
body { margin: 0; }
button { font: inherit; color: inherit; }
/* ---------- stage: fill the viewport, or show a device frame ---------- */
.stage {
/* safe-area insets: real ones when filling a phone, simulated in the frames */
--st: env(safe-area-inset-top, 0px);
--sb: env(safe-area-inset-bottom, 0px);
--sl: env(safe-area-inset-left, 0px);
--sr: env(safe-area-inset-right, 0px);
min-block-size: 100dvh;
display: grid;
place-content: center;
justify-items: center;
}
.stage[data-device="portrait"] { --st: 59px; --sb: 34px; --sl: 0px; --sr: 0px; }
.stage[data-device="landscape"] { --st: 0px; --sb: 21px; --sl: 59px; --sr: 59px; }
.stage[data-device="fill"] { display: block; }
.stage[data-device="fill"] .device { inline-size: 100%; block-size: 100dvh; }
.stage:not([data-device="fill"]) {
gap: 22px;
padding: 28px;
box-sizing: border-box;
background:
radial-gradient(60rem 40rem at 50% 0%, light-dark(#e3e6ee, #1b1d24), transparent),
light-dark(#d9dbe2, #0b0b0d);
}
.stage:not([data-device="fill"]) .device {
border-radius: 55px;
overflow: clip;
box-shadow:
0 0 0 11px light-dark(#1d1d1f, #2a2a2c),
0 0 0 13px light-dark(#8e8e93, #48484a),
0 50px 90px -30px rgb(0 0 0 / 0.55);
transition: inline-size 0.5s var(--ease), block-size 0.5s var(--ease);
}
.stage[data-device="portrait"] .device { inline-size: 393px; block-size: min(852px, calc(100dvh - 110px)); }
.stage[data-device="landscape"] .device { inline-size: min(852px, calc(100vw - 80px)); block-size: 393px; }
.caption {
display: none;
margin: 0;
font: 500 13px/1.4 var(--mono);
color: var(--label-2);
text-align: center;
}
.stage:not([data-device="fill"]) .caption { display: block; }
/* ---------- app shell: optional sidebar + the pane ---------- */
.app {
--margin: 16px;
block-size: 100%;
display: grid;
grid-template: minmax(0, 1fr) / minmax(0, 1fr);
background: var(--bg);
overflow: clip;
font-size: calc(17px * var(--dt-scale));
}
.app[data-size-class="regular"] { --margin: 20px; }
.app[data-sidebar] { grid-template-columns: 320px minmax(0, 1fr); }
.screen-guide { grid-area: 1 / 1 / 2 / -1; pointer-events: none; }
.pane { grid-row: 1; grid-column: -2 / -1; display: grid; grid-template: minmax(0, 1fr) / minmax(0, 1fr); overflow: clip; timeline-scope: --page; }
.safe-guide {
grid-area: 1 / 1;
margin: var(--st) var(--sr) var(--sb) var(--sl);
pointer-events: none;
}
.app[data-sidebar] .safe-guide { margin-inline-start: 0; }
.scroller {
grid-area: 1 / 1;
overflow-y: auto;
overscroll-behavior: contain;
scroll-timeline: --page block;
}
.content {
min-block-size: 100%;
padding-block-start: calc(var(--st) + 44px);
box-sizing: border-box;
}
/* the guides' own boxes (UIKit gives you these; you constrain TO them) */
.margins-guide { margin-inline: calc(var(--sl) + var(--margin)) calc(var(--sr) + var(--margin)); }
.app[data-sidebar] .margins-guide { margin-inline-start: var(--margin); }
.readable-guide { max-inline-size: 672px; margin-inline: auto; }
/* ---------- large title header ---------- */
.large-header { padding-block: 6px var(--space); }
.eyebrow {
margin: 0;
font-size: 0.78em;
font-weight: 600;
letter-spacing: 0.01em;
text-transform: uppercase;
color: var(--label-2);
}
.large-title {
inline-size: fit-content;
margin: 0;
font-size: 2em;
font-weight: 700;
letter-spacing: 0.01em;
line-height: 1.2;
}
.avatar {
inline-size: 36px;
block-size: 36px;
border: 0;
border-radius: 50%;
background: linear-gradient(135deg, #8e8e93, #636366);
color: #fff;
font: 600 14px/1 system-ui, sans-serif;
cursor: pointer;
}
/* ---------- cards ---------- */
.card {
background: var(--card);
border-radius: 13px;
padding: 12px 16px 16px;
scroll-margin-block-start: calc(var(--st) + 56px);
}
/* standard spacing between siblings in the chain */
.card[data-⚓️-side="below"] { margin-block-start: var(--space); }
.card[data-⚓️-side="after"] { margin-inline-start: var(--space); }
/* fillProportionally: half the readable width, minus half the gutter */
.app[data-size-class="regular"] .card[data-span="half"] { --⚓️-mult-x: 0.5; --⚓️-offset-x: -4px; }
.card:has(.card-hit:active) { scale: 0.985; }
.card { transition-property: inset, width, height, translate, scale; }
/* no reflow animation for the initial trait collection */
.app[data-booting] .card { transition: none; }
.card-hit {
z-index: 1;
margin: 0;
padding: 0;
border: 0;
border-radius: inherit;
background: transparent;
cursor: pointer;
}
.card-hit:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }
.card-head {
display: flex;
align-items: center;
min-block-size: 1.35em;
}
.card-title {
min-inline-size: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 0.9em;
font-weight: 600;
color: var(--tint);
}
.card-title svg {
inline-size: 1.05em;
block-size: 1.05em;
margin-inline-end: 0.35em;
vertical-align: -0.16em;
fill: currentColor;
stroke: currentColor;
stroke-width: 1.2;
stroke-linejoin: round;
}
.card:is(#c5) .card-title svg { fill: none; stroke-width: 2; stroke-linecap: round; }
/* value.leading >= title.trailing + 8 — the box is the ">=" slack, the text hugs its end */
.card-meta {
display: flex;
justify-content: flex-end;
align-items: center;
margin-inline-start: 8px;
overflow: hidden;
font-size: 0.8em;
color: var(--label-2);
white-space: nowrap;
}
.card-meta > span { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; }
.card-meta::after { content: "›"; flex: none; margin-inline-start: 0.4em; font-size: 1.35em; line-height: 1; color: var(--label-3); }
.metric {
inline-size: fit-content;
display: flex;
align-items: baseline;
gap: 0.18em;
margin: 10px 0 10px;
white-space: nowrap;
}
.num { font: 700 1.6em/1 ui-rounded, system-ui, sans-serif; letter-spacing: -0.01em; }
.unit { font-size: 0.82em; font-weight: 600; color: var(--label-2); }
.badge {
margin-inline-start: 8px;
padding: 0.2em 0.55em;
border-radius: 999px;
background: color-mix(in srgb, var(--tint) 15%, transparent);
color: var(--tint);
font-size: 0.7em;
font-weight: 600;
white-space: nowrap;
}
/* mini charts (all 56px tall so rows line up) */
.bars, .ranges, .stages, .dots, .line, .glass-wrap { block-size: 56px; }
.bars { display: flex; align-items: flex-end; gap: 5px; }
.bars i { flex: 1; block-size: calc(var(--v) * 100%); border-radius: 4px; background: color-mix(in srgb, var(--tint) 40%, transparent); }
.bars i:last-child { background: var(--tint); }
.ranges { display: flex; align-items: flex-start; justify-content: space-between; }
.ranges i { inline-size: 7px; margin-block-start: calc((1 - var(--b)) * 56px); block-size: calc((var(--b) - var(--a)) * 56px); border-radius: 4px; background: var(--tint); }
.stages { display: flex; align-items: flex-start; }
.stages i { flex: var(--w); block-size: 11px; margin-block-start: calc(var(--y) * 15px); border-radius: 3px; background: var(--tint); opacity: calc(1 - var(--y) * 0.18); }
.line { display: block; inline-size: 100%; overflow: visible; }
.line polyline { fill: none; stroke: var(--tint); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.dots { display: flex; align-items: center; gap: 8px; }
.dots i { inline-size: 16px; block-size: 16px; border-radius: 50%; background: var(--tint); }
.dots i.off { background: var(--fill); }
.glass { block-size: 12px; margin-block: 22px; border-radius: 6px; background: var(--fill); overflow: hidden; }
.glass i { display: block; block-size: 100%; inline-size: calc(var(--v) * 100%); border-radius: inherit; background: var(--tint); }
/* activity */
.activity { display: flex; align-items: center; gap: 16px; margin-block-start: 10px; }
.rings { block-size: 96px; aspect-ratio: 1; flex: none; rotate: -90deg; }
.rings circle { fill: none; stroke-width: 10; stroke-linecap: round; }
.rings .m { stroke: var(--move); }
.rings .e { stroke: var(--exercise); }
.rings .s { stroke: var(--stand); }
.rings .track { opacity: 0.22; }
.stats { flex: 1; min-inline-size: 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 0; }
.stats dt { font-size: 0.72em; font-weight: 600; color: var(--label-2); }
.stats dd { margin: 2px 0 0; font: 700 1.3em/1.1 ui-rounded, system-ui, sans-serif; color: var(--tint); }
.stats small { display: block; margin-block-start: 1px; font-size: 0.55em; font-weight: 600; white-space: nowrap; }
.bottom-inset { inline-size: 1px; block-size: calc(var(--sb) + 88px); pointer-events: none; }
/* ---------- navigation bar (fades in as the large title scrolls away) ---------- */
.navbar {
z-index: 3;
block-size: calc(var(--st) + 44px);
background: color-mix(in srgb, var(--bg) 72%, transparent);
backdrop-filter: blur(20px) saturate(180%);
border-block-end: 0.5px solid var(--sep);
pointer-events: none;
}
.inline-title {
z-index: 4;
margin: 0;
block-size: 44px;
line-height: 44px;
font-weight: 600;
pointer-events: none;
}
@keyframes nav-in { from { opacity: 0; } to { opacity: 1; } }
.navbar, .inline-title {
opacity: 0;
animation: nav-in linear both;
animation-timeline: --page;
animation-range: 40px 72px;
}
/* ---------- floating toolbar ---------- */
.pill {
z-index: 5;
display: flex;
gap: 2px;
margin-block-end: 8px;
padding: 5px;
border-radius: 999px;
background: var(--glass);
backdrop-filter: blur(24px) saturate(180%);
box-shadow: 0 0 0 0.5px var(--sep), 0 10px 30px -10px rgb(0 0 0 / 0.35);
font-size: 14px;
}
.pill-btn {
display: flex;
align-items: center;
gap: 6px;
min-block-size: 36px;
padding: 0 12px;
border: 0;
border-radius: 999px;
background: transparent;
font-weight: 600;
white-space: nowrap;
cursor: pointer;
transition: background-color 0.2s, color 0.2s;
}
.pill-btn svg { inline-size: 18px; block-size: 18px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; }
.pill-btn:hover { background: var(--fill); }
.pill-btn[aria-pressed="true"] { background: var(--blue); color: #fff; }
.pill-btn:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
.pill-btn .aa { font: 700 15px/1 ui-rounded, system-ui, sans-serif; }
.pill-btn small { font-weight: 500; color: var(--label-2); }
@media (width < 900px) { #rotate { display: none; } }
.stage[data-device="portrait"] :is(#rotate span, #dt .dt-label) { display: none; }
@media (width < 380px) { .pill-btn { padding: 0 9px; } #dt .dt-label { display: none; } }
/* ---------- sidebar (regular width, split-view primary column) ---------- */
.sidebar {
display: none;
grid-row: 1;
grid-column: 1;
overflow-y: auto;
padding: calc(var(--st) + 44px) 16px 24px;
background: light-dark(#f2f2f7, #1c1c1e);
border-inline-end: 0.5px solid var(--sep);
}
.app[data-sidebar] .sidebar { display: block; }
.sidebar-title { margin: 0 0 10px; font-size: 2em; font-weight: 700; }
.search {
display: flex;
align-items: center;
gap: 6px;
padding: 0 8px;
block-size: 36px;
border-radius: 10px;
background: var(--fill);
color: var(--label-2);
}
.search svg { inline-size: 16px; block-size: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.search input { flex: 1; min-inline-size: 0; border: 0; background: none; font: inherit; color: var(--label); outline: none; }
.side-heading { margin: 22px 0 6px; padding-inline: 8px; font-size: 1.15em; font-weight: 700; }
.side-list { list-style: none; margin: 14px 0 0; padding: 0; }
.side-item {
display: flex;
align-items: center;
gap: 12px;
inline-size: 100%;
min-block-size: 44px;
padding: 0 8px;
border: 0;
border-radius: 10px;
background: none;
text-align: start;
cursor: pointer;
}
.side-item:hover { background: var(--fill); }
.side-item.is-current { background: var(--blue); color: #fff; }
.side-item:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; }
.side-icon {
display: grid;
place-items: center;
inline-size: 29px;
block-size: 29px;
border-radius: 7px;
background: var(--tint);
color: #fff;
font-size: 15px;
}
/* ---------- simulated hardware ---------- */
.island, .status-time, .status-icons, .home-indicator { display: none; z-index: 9; pointer-events: none; }
.stage:not([data-device="fill"]) :is(.island, .home-indicator) { display: block; }
.stage[data-device="portrait"] :is(.status-time, .status-icons) { display: flex; }
.island { inline-size: 124px; block-size: 36px; margin-block-start: 11px; border-radius: 20px; background: #000; }
.stage[data-orient="landscape"] .island { inline-size: 36px; block-size: 124px; margin-block-start: 0; margin-inline-start: 11px; }
.status-time { margin-inline-start: 50px; font: 600 17px/1 system-ui, sans-serif; letter-spacing: -0.01em; }
.status-icons { align-items: center; gap: 5px; margin-inline-end: 32px; }
.status-icons i { inline-size: 17px; block-size: 11px; border-radius: 2px; background: linear-gradient(to right, currentColor 0 22%, transparent 22% 26%, currentColor 26% 48%, transparent 48% 52%, currentColor 52% 74%, transparent 74% 78%, currentColor 78%); mask: linear-gradient(to top left, #000 50%, transparent 50%); }
.status-icons i + i { inline-size: 15px; mask: radial-gradient(circle at 50% 100%, #000 60%, transparent 61%); background: currentColor; }
.status-icons b { inline-size: 25px; block-size: 12px; border: 1.5px solid color-mix(in srgb, currentColor 40%, transparent); border-radius: 4px; box-sizing: border-box; background: linear-gradient(currentColor, currentColor) 1.5px 50% / 70% 7px no-repeat; }
.home-indicator { inline-size: 134px; block-size: 5px; margin-block-end: 8px; border-radius: 3px; background: var(--label); }
.stage[data-orient="landscape"] .home-indicator { inline-size: 200px; }
/* ---------- X-ray (on while the inspector is open) ---------- */
.xr-band { display: none; z-index: 2; pointer-events: none; }
.app.xray .xr-band { display: block; }
.xr-margins { background: repeating-linear-gradient(-45deg, color-mix(in srgb, var(--pink) 9%, transparent) 0 6px, transparent 6px 12px); box-shadow: inset 1px 0 var(--pink), inset -1px 0 var(--pink); }
.xr-readable { background: color-mix(in srgb, var(--blue) 7%, transparent); box-shadow: inset 1px 0 var(--blue), inset -1px 0 var(--blue); }
.app.xray .safe-guide { outline: 1.5px dashed var(--orange); outline-offset: -1.5px; }
.app.xray :is(.card, .large-title, .avatar, .pill, .inline-title) { outline: 1px dashed var(--green); outline-offset: 1px; }
.app.xray .card-meta { outline: 1px dashed var(--orange); outline-offset: -1px; }
.app.xray .card::before {
content: "side=" attr(data-⚓️-side);
position: absolute;
inset-block-start: -8px;
inset-inline-end: 10px;
z-index: 2;
padding: 1px 6px;
border-radius: 6px;
background: var(--green);
color: #000;
font: 600 10px/1.4 var(--mono);
}
/* inspector hover highlight */
.ix-hl { outline: 2.5px solid var(--blue) !important; outline-offset: 2px !important; }
.xr-band.ix-hl, .safe-guide.ix-hl { outline-offset: -2.5px !important; background-color: color-mix(in srgb, var(--blue) 16%, transparent); }
/* ---------- constraints inspector ---------- */
.inspector {
inline-size: min(34rem, calc(100vw - 24px));
max-block-size: min(58dvh, 32rem);
margin-block-end: 10px;
padding: 0;
overflow: auto;
border: 0;
border-radius: 22px;
background: color-mix(in srgb, var(--card) 88%, transparent);
backdrop-filter: blur(24px) saturate(180%);
box-shadow: 0 0 0 0.5px var(--sep), 0 24px 60px -20px rgb(0 0 0 / 0.5);
color: var(--label);
font-size: 13px;
}
.inspector-head {
position: sticky;
inset-block-start: 0;
z-index: 1;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 14px 18px 10px;
background: inherit;
backdrop-filter: inherit;
border-block-end: 0.5px solid var(--sep);
}
.inspector-head strong { font-size: 17px; }
.traits { font: 500 12px var(--mono); color: var(--label-2); }
.ix-list { list-style: none; margin: 0; padding: 6px 8px; }
.ix-row {
display: grid;
gap: 2px;
inline-size: 100%;
padding: 8px 10px;
border: 0;
border-radius: 10px;
background: none;
text-align: start;
cursor: pointer;
}
.ix-row:hover, .ix-row:focus-visible { background: color-mix(in srgb, var(--blue) 12%, transparent); outline: none; }
.ix-name { font-weight: 600; font-size: 13px; }
.ix-row code { font: 12px/1.45 var(--mono); color: var(--label-2); overflow-wrap: anywhere; }
.ix-row code.note { color: var(--orange); }
.ix-foot { margin: 0; padding: 4px 18px 16px; color: var(--label-2); font-size: 12px; }
.ix-foot code, .ix-row code .k { font-family: var(--mono); }
/* ---------- detail sheet ---------- */
.sheet {
padding: 0 0 24px;
border: 0;
overflow: auto;
background: var(--bg);
color: var(--label);
font-size: calc(17px * var(--dt-scale));
box-shadow: 0 -10px 40px -10px rgb(0 0 0 / 0.35);
}
.sheet::backdrop { background: rgb(0 0 0 / 0.32); }
.sheet[data-style="bottom"] { border-radius: 13px 13px 0 0; --⚓️-mult-y: 0.72; }
/* :where() keeps this below the library's @starting-style so the sheet can grow out of the card */
.sheet:where([data-style="form"]) { inline-size: min(540px, calc(100vw - 40px)); max-block-size: 640px; border-radius: 13px; --⚓️-mult-y: 0.8; }
.grabber { inline-size: 36px; block-size: 5px; margin: 6px auto 0; border-radius: 3px; background: var(--label-3); }
.sheet[data-style="form"] .grabber { visibility: hidden; }
.sheet-bar { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 6px 16px 10px; }
.sheet-bar h2 { grid-column: 2; margin: 0; font-size: 1em; font-weight: 600; }
.sheet-bar form { grid-column: 3; justify-self: end; }
.done { padding: 6px 0; border: 0; background: none; color: var(--blue); font-weight: 600; cursor: pointer; }
.sheet-content { padding-inline: 16px; }
.sheet-content .card-body { padding: 14px 16px 16px; border-radius: 13px; background: var(--card); }
.sheet-content .bars, .sheet-content .ranges, .sheet-content .line, .sheet-content .stages, .sheet-content .dots { block-size: 140px; }
.sheet-content .ranges i { margin-block-start: calc((1 - var(--b)) * 140px); block-size: calc((var(--b) - var(--a)) * 140px); }
.sheet-content .stages i { margin-block-start: calc(var(--y) * 38px); block-size: 24px; }
.group { list-style: none; margin: 20px 0 0; padding: 0; border-radius: 13px; background: var(--card); overflow: hidden; }
.group li { display: flex; justify-content: space-between; padding: 12px 16px; }
.group li + li { box-shadow: inset 0 0.5px var(--sep); }
.group li span:last-child { color: var(--label-2); }
@media (prefers-reduced-motion: reduce) {
.card, .pill-btn, .stage:not([data-device="fill"]) .device { transition: none; }
.card:has(.card-hit:active) { scale: none; }
}// State only. JS decides the trait collection (compact vs regular) and swaps
// which constraint set is active — like traitCollectionDidChange activating one
// NSLayoutConstraint array and deactivating another. Positions and sizes all come
// from data-⚓️-* attributes; nothing here measures or places a view.
const S = "data-⚓️-";
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
const stage = $(".stage");
const app = $(".app");
const pane = $(".pane");
const cards = $$(".card");
const inspector = $("#inspector");
const list = $(".ix-list");
const sheet = $(".sheet");
const reduce = matchMedia("(prefers-reduced-motion: reduce)");
const state = { size: "auto", device: "fill", dt: 0 };
const SIZES = ["auto", "compact", "regular"];
const DEVICES = ["fill", "portrait", "landscape"];
const DT = [["Default", 1], ["Large", 1.18], ["AX1", 1.4]];
const attr = (el, name, value) =>
value == null || value === false ? el.removeAttribute(S + name) : el.setAttribute(S + name, value === true ? "" : value);
/* ---------- the two constraint sets ---------- */
function activateConstraints(regular) {
for (const card of cards) {
const after = regular && card.dataset.regular === "after";
attr(card, "side", after ? "after" : "below"); // top = prev.bottom + 8 | leading = prev.trailing + 8
attr(card, "align-top", after); // top = prev.top
attr(card, "sync-height", after); // height = prev.height
attr(card, "align-leading", after ? null : "--readable"); // leading = readable.leading
}
}
function traits() {
const sidebar = state.device === "fill" && app.clientWidth >= 1000;
app.toggleAttribute("data-sidebar", sidebar);
const auto = pane.clientWidth >= 600 ? "regular" : "compact";
const h = pane.clientHeight >= 500 ? "regular" : "compact";
return { sidebar, w: state.size === "auto" ? auto : state.size, h, auto };
}
function update() {
const t = traits();
if (app.dataset.sizeClass !== t.w) {
app.dataset.sizeClass = t.w;
activateConstraints(t.w === "regular");
}
app.dataset.vsize = t.h;
if (sheet.open) layoutSheet();
const sc = $("#sizeclass span");
sc.textContent = state.size === "auto" ? `Auto · ${t.auto === "regular" ? "R" : "C"}` : state.size[0].toUpperCase() + state.size.slice(1);
$("#sizeclass").setAttribute("aria-label", `Size class: ${state.size} (${t.w} width)`);
$("#rotate span").textContent = { fill: "Fill", portrait: "Portrait", landscape: "Landscape" }[state.device];
$("#rotate").setAttribute("aria-label", `Device: ${state.device}`);
$("#dt .dt-label").textContent = DT[state.dt][0];
const code = `w${t.w[0].toUpperCase()} h${t.h[0].toUpperCase()}`;
$(".traits").textContent = `${code} · ${pane.clientWidth}×${pane.clientHeight}`;
$(".caption").textContent =
`iPhone · ${state.device} · ${code} · ${t.w === "regular" ? "2-up cards" : "1-up stack"}`;
if (inspector.matches(":popover-open")) renderInspector();
}
new ResizeObserver(update).observe(app);
addEventListener("resize", () => {
if (innerWidth < 900 && state.device !== "fill") setDevice("fill");
requestAnimationFrame(() => requestAnimationFrame(() => app.removeAttribute("data-booting")));
});
/* ---------- controls ---------- */
function setDevice(d) {
state.device = d;
stage.dataset.device = d;
const land = d === "landscape";
stage.dataset.orient = land ? "landscape" : "portrait";
// Dynamic Island: centerX + top in portrait, leading + centerY in landscape
const island = $(".island");
attr(island, "align-top", !land);
attr(island, "align-x", land ? null : "center");
attr(island, "align-leading", land);
attr(island, "align-y", land ? "center" : null);
update();
}
$("#sizeclass").addEventListener("click", () => {
state.size = SIZES[(SIZES.indexOf(state.size) + 1) % SIZES.length];
update();
});
$("#rotate").addEventListener("click", () => setDevice(DEVICES[(DEVICES.indexOf(state.device) + 1) % DEVICES.length]));
$("#dt").addEventListener("click", () => {
state.dt = (state.dt + 1) % DT.length;
document.documentElement.style.setProperty("--dt-scale", DT[state.dt][1]);
update();
});
$("#inspect").addEventListener("click", () => toggleInspector());
addEventListener("keydown", (e) => {
if (e.key === "Escape" && inspector.matches(":popover-open")) toggleInspector(false);
});
function toggleInspector(force) {
const open = force ?? !inspector.matches(":popover-open");
app.classList.toggle("xray", open);
$("#inspect").setAttribute("aria-pressed", open);
if (open) {
renderInspector();
inspector.showPopover();
} else {
inspector.hidePopover();
clearHighlight();
}
}
// sidebar: jump to a card
$(".sidebar").addEventListener("click", (e) => {
const item = e.target.closest(".side-item");
if (!item) return;
$$(".side-item").forEach((b) => b.classList.toggle("is-current", b === item));
const behavior = reduce.matches ? "auto" : "smooth";
if (item.dataset.go === "top") $(".scroller").scrollTo({ top: 0, behavior });
else document.getElementById(item.dataset.go).scrollIntoView({ block: "start", behavior });
});
/* ---------- detail sheet ---------- */
function layoutSheet() {
const regular = app.dataset.sizeClass === "regular";
sheet.dataset.style = regular ? "form" : "bottom";
attr(sheet, "align-bottom", !regular); // bottom sheet: bottom = view.bottom, leading/trailing = view
attr(sheet, "stretch-x", !regular);
attr(sheet, "center-all", regular); // form sheet: centerX/centerY = view
attr(sheet, "sync-height", true); // height = 0.72 × view.height (0.8 for form sheets)
}
pane.addEventListener("click", (e) => {
const hit = e.target.closest(".card-hit");
if (!hit) return;
const card = hit.closest(".card");
layoutSheet();
attr(sheet, "morph-from", card.getAttribute(S + "name")); // grow out of the tapped card
$("#sheet-title").textContent = card.querySelector(".card-title").textContent;
const body = card.querySelector(".card-body").cloneNode(true);
const rows = [["Daily Average", "Last 7 days"], ["Show All Data", "›"], ["Data Sources & Access", "›"], ["Add to Favourites", "★"]];
const group = document.createElement("ul");
group.className = "group";
group.innerHTML = rows.map(([a, b]) => `<li><span>${a}</span><span>${b}</span></li>`).join("");
$(".sheet-content").replaceChildren(body, group);
sheet.style.setProperty("--tint", card.style.getPropertyValue("--tint"));
sheet.showModal();
});
/* ---------- constraints inspector (reads the live attributes) ---------- */
const GUIDES = { "--readable": "readableContentGuide", "--margins": "layoutMarginsGuide", "--safe": "safeAreaLayoutGuide", "--pane": "view", "--screen": "window" };
function resolve(from, name) {
const scope = from.closest(`[${S}scope]`);
return (scope && scope.querySelector(`[${S}name="${name}"]`)) || document.querySelector(`[${S}name="${name}"]`);
}
function labelOf(from, name) {
const el = resolve(from, name);
if (el?.dataset.ix) return el.dataset.ix;
if (GUIDES[name]) return GUIDES[name];
const card = from.closest(".card");
const prefix = card ? card.dataset.ix.replace(/Card$/, "") + "." : "";
return prefix + ({ "--num": "value", "--head": "header" }[name] ?? name.slice(2));
}
const px = (v) => Math.round(parseFloat(v) || 0);
const k = (n) => (n === 0 ? "" : n > 0 ? ` + ${n}` : ` − ${-n}`);
const fmt = (m) => (Math.abs(m - 1) < 1e-6 ? "" : `${+m.toFixed(3)} × `);
function constraintsFor(el) {
const me = el.dataset.ix;
const cs = getComputedStyle(el);
const a = (n) => el.getAttribute(S + n);
const has = (n) => el.hasAttribute(S + n);
const to = (v) => labelOf(el, v || a("to"));
const mbs = px(cs.marginBlockStart), mbe = px(cs.marginBlockEnd);
const mis = px(cs.marginInlineStart), mie = px(cs.marginInlineEnd);
const out = [];
const side = a("side");
if (side === "below") out.push(`${me}.top = ${to()}.bottom${k(mbs)}`);
if (side === "above") out.push(`${me}.bottom = ${to()}.top${k(-mbe)}`);
if (side === "after") out.push(`${me}.leading = ${to()}.trailing${k(mis)}`);
if (side === "before") out.push(`${me}.trailing = ${to()}.leading${k(-mie)}`);
if (has("after")) out.push(`${me}.leading ${has("align-trailing") ? "≥" : "="} ${to(a("after"))}.trailing${k(mis + px(cs.paddingInlineStart))}`);
if (has("align-top")) out.push(`${me}.top = ${to(a("align-top"))}.top${k(mbs)}`);
if (has("align-bottom")) out.push(`${me}.bottom = ${to(a("align-bottom"))}.bottom${k(-mbe)}`);
if (has("align-leading")) out.push(`${me}.leading = ${to(a("align-leading"))}.leading${k(mis)}`);
if (has("align-trailing")) out.push(`${me}.trailing = ${to(a("align-trailing"))}.trailing${k(-mie)}`);
if (has("pin-top")) out.push(`${me}.top = superview.top`);
if (has("pin-x")) out.push(`${me}.leading = superview.leading`, `${me}.trailing = superview.trailing`);
if (a("align-x") === "center") out.push(`${me}.centerX = ${to()}.centerX`);
if (a("align-y") === "center") out.push(`${me}.centerY = ${to()}.centerY`);
if (has("sync-width")) out.push(`${me}.width = ${fmt(parseFloat(cs.getPropertyValue("--⚓️-mult-x")) || 1)}${to(a("sync-width"))}.width${k(px(cs.getPropertyValue("--⚓️-offset-x")))}`);
if (has("sync-height")) out.push(`${me}.height = ${to(a("sync-height"))}.height`);
const p = el.dataset.ixPriority;
return p ? out.map((l) => `${l} @${p}`) : out;
}
function guideLines(el) {
const cs = getComputedStyle(el);
const me = el.dataset.ix;
if (me === "safeAreaLayoutGuide")
return [`insets = (top ${px(cs.marginTop)}, leading ${px(cs.marginInlineStart)}, bottom ${px(cs.marginBottom)}, trailing ${px(cs.marginInlineEnd)})`];
const margin = px(getComputedStyle(app).getPropertyValue("--margin"));
if (me === "layoutMarginsGuide")
return [`${me}.leading = safeAreaLayoutGuide.leading + ${margin}`, `${me}.trailing = safeAreaLayoutGuide.trailing − ${margin}`];
if (me === "readableContentGuide")
return [`${me}.width ≤ 672 (now ${el.clientWidth})`, `${me}.centerX = layoutMarginsGuide.centerX`, `${me}.leading ≥ layoutMarginsGuide.leading`];
if (me === "rings") return [`rings.width = rings.height (aspectRatio 1:1)`];
if (me === "largeTitle") return [`largeTitle.leading = readableContentGuide.leading (in flow)`];
return [];
}
const KEYS = ["safeAreaLayoutGuide", "layoutMarginsGuide", "readableContentGuide", "largeTitle", "avatar", "inlineTitle",
"activityCard", "rings", "stepsCard", "steps.title", "steps.time", "steps.trend", "heartCard", "sleepCard",
"distanceCard", "distance.title", "distance.time", "distance.trend", "mindfulCard", "waterCard", "toolbar"];
function renderInspector() {
list.replaceChildren(
...KEYS.map((key) => {
const el = $(`[data-ix="${key}"]`);
const lines = [...guideLines(el), ...constraintsFor(el)];
const notes = (el.dataset.ixExtra ?? "").split(";").filter(Boolean);
const li = document.createElement("li");
const b = document.createElement("button");
b.className = "ix-row";
b.dataset.key = key;
b.innerHTML = `<span class="ix-name">${key}</span>` +
lines.map((l) => `<code>${l}</code>`).join("") +
notes.map((n) => `<code class="note">${n.trim()}</code>`).join("");
li.append(b);
return li;
}),
);
}
let lit = [];
function clearHighlight() {
lit.forEach((el) => el.classList.remove("ix-hl"));
lit = [];
}
function highlight(key, scroll) {
clearHighlight();
const el = $(`[data-ix="${key}"]`);
const band = { layoutMarginsGuide: ".xr-margins", readableContentGuide: ".xr-readable" }[key];
lit = [band ? $(band) : el];
lit.forEach((n) => n.classList.add("ix-hl"));
if (scroll && !band && el.closest(".scroller")) el.scrollIntoView({ block: "nearest", behavior: reduce.matches ? "auto" : "smooth" });
}
list.addEventListener("pointerover", (e) => { const r = e.target.closest(".ix-row"); if (r) highlight(r.dataset.key); });
list.addEventListener("pointerleave", clearHighlight);
list.addEventListener("focusin", (e) => { const r = e.target.closest(".ix-row"); if (r) highlight(r.dataset.key, true); });
list.addEventListener("focusout", clearHighlight);
list.addEventListener("click", (e) => { const r = e.target.closest(".ix-row"); if (r) highlight(r.dataset.key, true); });
// initial trait collection before first paint (no glide on load)
setDevice("fill");
requestAnimationFrame(() => requestAnimationFrame(() => app.removeAttribute("data-booting")));data-⚓️-afterdata-⚓️-align-bottomdata-⚓️-align-leadingdata-⚓️-align-topdata-⚓️-align-trailingdata-⚓️-align-xdata-⚓️-align-ydata-⚓️-morph-fromdata-⚓️-namedata-⚓️-pin-alldata-⚓️-pin-topdata-⚓️-pin-xdata-⚓️-pin-ydata-⚓️-rootdata-⚓️-scopedata-⚓️-sidedata-⚓️-stretch-xdata-⚓️-sync-widthdata-⚓️-todata-⚓️-transitiondata-⚓️-visibility