iOS profile with proportional constraints
A UIKit-style profile screen built from Auto Layout constraints written as data-⚓️-* attributes, with proportional widths from sync-width and --⚓️-mult-x.
<div class="page" data-device="portrait" data-sc="compact">
<header class="toolbar">
<div class="brand">
<strong>Proportional profile</strong>
<span>UIKit Auto Layout, written as <code>data-⚓️-*</code></span>
</div>
<fieldset class="seg" aria-label="Device and size class">
<legend>Device</legend>
<label><input type="radio" name="device" value="portrait" checked> <span>Portrait</span></label>
<label><input type="radio" name="device" value="landscape"> <span>Landscape</span></label>
<label><input type="radio" name="device" value="ipad"> <span>iPad</span></label>
</fieldset>
<output class="trait" aria-live="polite">compact</output>
<label class="switch"><input type="checkbox" name="inspect" role="switch"> Constraints</label>
</header>
<main class="stage">
<div class="stage-main">
<div class="device">
<div class="screen app" data-⚓️-root data-⚓️-scope="all" data-size-class="compact">
<div class="scroll" data-⚓️-root tabindex="0" aria-label="Mara Okafor profile">
<!-- Layout guides (UILayoutGuide): invisible, only ever anchors -->
<div class="g g-superview" data-⚓️-name="--superview" data-⚓️-pin-x data-⚓️-pin-top data-label="superview"></div>
<div class="g g-margins" data-⚓️-name="--margins" data-⚓️-pin-x data-⚓️-pin-top data-label="layoutMarginsGuide"></div>
<div class="cover" data-⚓️-name="--cover" data-⚓️-pin-x data-⚓️-pin-top data-label="cover" role="img" aria-label="Foggy ridgeline cover photo"></div>
<div class="g g-edge" data-⚓️-name="--edge" data-⚓️-to="--cover" data-⚓️-stretch-x data-⚓️-align-bottom data-label="cover.bottom"></div>
<div class="g g-col" data-⚓️-name="--col" data-⚓️-to="--margins" data-⚓️-align-leading data-⚓️-sync-width="--margins" data-label="column"></div>
<div class="avatar" data-⚓️-name="--avatar" data-⚓️-to="--edge" data-⚓️-sync-width="--superview"
data-⚓️-center-all
data-compact="center-all" data-regular="align-y=center align-leading=--margins"
data-label="avatar" role="img" aria-label="Mara Okafor avatar"><span>MO</span></div>
<h1 class="name" data-⚓️-name="--name" data-⚓️-to="--avatar"
data-⚓️-below data-⚓️-align-x="center"
data-compact="below align-x=center" data-regular="after below=--edge"
data-label="nameLabel">Mara Okafor</h1>
<div class="badge" data-⚓️-name="--badge" data-⚓️-to="--name" data-⚓️-after data-⚓️-align-top data-⚓️-sync-height data-label="verifiedBadge"><svg viewBox="0 0 24 24" aria-label="Verified" role="img"><path fill="currentColor" d="M12 1.5l2.6 1.9 3.2-.2 1 3.1 2.6 1.9-1 3.1 1 3.1-2.6 1.9-1 3.1-3.2-.2L12 22.5l-2.6-1.9-3.2.2-1-3.1-2.6-1.9 1-3.1-1-3.1 2.6-1.9 1-3.1 3.2.2z"/><path fill="none" stroke="var(--bg)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" d="M7.6 12.2l3 3 5.8-6.2"/></svg></div>
<p class="handle" data-⚓️-name="--handle" data-⚓️-to="--name" data-⚓️-below
data-⚓️-align-x="center"
data-compact="align-x=center" data-regular="align-leading"
data-label="handleLabel">@mara.fog</p>
<p class="bio" data-⚓️-name="--bio" data-⚓️-to="--handle" data-⚓️-below data-⚓️-stretch-x="--col"
data-label="bioLabel">Landscape photographer chasing fog along the Pacific Northwest coast. Film when it's slow, digital when it isn't.</p>
<div class="stats" data-⚓️-name="--stats" data-⚓️-to="--bio" data-⚓️-below data-⚓️-stretch-x="--col" data-label="statsStack"></div>
<div class="stat" data-⚓️-name="--stat1" data-⚓️-to="--stats" data-⚓️-align-leading data-⚓️-stretch-y data-⚓️-sync-width data-label="stat[0]"><b>248</b><span>Posts</span></div>
<div class="stat" data-⚓️-name="--stat2" data-⚓️-to="--stat1" data-⚓️-after data-⚓️-stretch-y data-⚓️-sync-width="--stats" data-label="stat[1]"><b class="followers">12.4K</b><span>Followers</span></div>
<div class="stat" data-⚓️-name="--stat3" data-⚓️-to="--stat2" data-⚓️-after data-⚓️-stretch-y data-⚓️-sync-width="--stats" data-label="stat[2]"><b>311</b><span>Following</span></div>
<div class="divider" data-⚓️-name="--div1" data-⚓️-to="--stat2" data-⚓️-align-leading data-⚓️-stretch-y data-label="divider"></div>
<div class="divider" data-⚓️-name="--div2" data-⚓️-to="--stat3" data-⚓️-align-leading data-⚓️-stretch-y data-label="divider"></div>
<button class="btn btn-follow" type="button" aria-pressed="false" data-⚓️-name="--follow" data-⚓️-to="--stats" data-⚓️-below data-⚓️-align-leading data-⚓️-sync-width data-label="followButton">Follow</button>
<button class="btn btn-message" type="button" data-⚓️-name="--message" data-⚓️-to="--follow" data-⚓️-after data-⚓️-align-top data-⚓️-sync-width data-⚓️-sync-height data-label="messageButton">Message</button>
<div class="g g-grid" data-⚓️-name="--grid" data-⚓️-to="--follow" data-⚓️-below data-⚓️-pin-x
data-compact="to=--follow below pin-x" data-regular="to=--col after below=--edge align-trailing=--margins"
data-label="photoGrid"></div>
<div class="cell" style="--h:210" data-⚓️-name="--p1" data-⚓️-to="--grid" data-⚓️-align-top data-⚓️-align-leading data-⚓️-sync-width="--grid" data-label="cell[0]"></div>
<div class="cell" style="--h:30" data-⚓️-name="--p2" data-⚓️-to="--p1" data-⚓️-after data-⚓️-align-top data-⚓️-sync-width="--grid" data-label="cell[1]"></div>
<div class="cell" style="--h:160" data-⚓️-name="--p3" data-⚓️-to="--p2" data-⚓️-after data-⚓️-align-top data-⚓️-sync-width="--grid" data-label="cell[2]"></div>
<div class="cell" style="--h:260" data-⚓️-name="--p4" data-⚓️-to="--p1" data-⚓️-below data-⚓️-align-leading data-⚓️-sync-width="--grid" data-label="cell[3]"></div>
<div class="cell" style="--h:80" data-⚓️-name="--p5" data-⚓️-to="--p4" data-⚓️-after data-⚓️-align-top data-⚓️-sync-width="--grid" data-label="cell[4]"></div>
<div class="cell" style="--h:190" data-⚓️-name="--p6" data-⚓️-to="--p5" data-⚓️-after data-⚓️-align-top data-⚓️-sync-width="--grid" data-label="cell[5]"></div>
<div class="cell" style="--h:320" data-⚓️-name="--p7" data-⚓️-to="--p4" data-⚓️-below data-⚓️-align-leading data-⚓️-sync-width="--grid" data-label="cell[6]"></div>
<div class="cell" style="--h:140" data-⚓️-name="--p8" data-⚓️-to="--p7" data-⚓️-after data-⚓️-align-top data-⚓️-sync-width="--grid" data-label="cell[7]"></div>
<div class="cell" style="--h:50" data-⚓️-name="--p9" data-⚓️-to="--p8" data-⚓️-after data-⚓️-align-top data-⚓️-sync-width="--grid" data-label="cell[8]"></div>
<div class="cell" style="--h:230" data-⚓️-name="--p10" data-⚓️-to="--p7" data-⚓️-below data-⚓️-align-leading data-⚓️-sync-width="--grid" data-label="cell[9]"></div>
<div class="cell" style="--h:110" data-⚓️-name="--p11" data-⚓️-to="--p10" data-⚓️-after data-⚓️-align-top data-⚓️-sync-width="--grid" data-label="cell[10]"></div>
<div class="cell" style="--h:0" data-⚓️-name="--p12" data-⚓️-to="--p11" data-⚓️-after data-⚓️-align-top data-⚓️-sync-width="--grid" data-label="cell[11]"></div>
<!-- bottom content inset: keeps the last row clear of the tab bar + home indicator -->
<div class="g g-spacer" data-⚓️-to="--p10" data-⚓️-below></div>
<!-- inspector highlight: fills whichever view is hovered, glides between them -->
<div class="hl" data-⚓️-fill="--avatar" data-⚓️-transition="glide" aria-hidden="true"></div>
</div>
<div class="statusbar" data-⚓️-pin-top data-⚓️-pin-x aria-hidden="true">
<span class="time">9:41</span>
<span class="glyphs">
<svg viewBox="0 0 18 12"><rect x="0" y="8" width="3" height="4" rx="1"/><rect x="5" y="5.5" width="3" height="6.5" rx="1"/><rect x="10" y="3" width="3" height="9" rx="1"/><rect x="15" y="0" width="3" height="12" rx="1"/></svg>
<svg viewBox="0 0 26 12"><rect x=".5" y=".5" width="22" height="11" rx="3.5" fill="none" stroke="currentColor" opacity=".4"/><rect x="2" y="2" width="16" height="8" rx="2"/><rect x="23.5" y="4" width="1.8" height="4" rx=".9" opacity=".4"/></svg>
</span>
</div>
<nav class="navbar" data-⚓️-pin-top data-⚓️-pin-x aria-label="Navigation bar">
<button class="glass" type="button" aria-label="Back"><svg viewBox="0 0 24 24"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<span class="nav-title">Mara Okafor</span>
<button class="glass" type="button" aria-label="More"><svg viewBox="0 0 24 24"><circle cx="5.5" cy="12" r="1.9"/><circle cx="12" cy="12" r="1.9"/><circle cx="18.5" cy="12" r="1.9"/></svg></button>
</nav>
<nav class="tabbar" data-⚓️-pin-bottom data-⚓️-pin-x aria-label="Tab bar">
<a href="#" aria-label="Home"><svg viewBox="0 0 24 24"><path d="M4 10.5L12 4l8 6.5V20h-5v-6H9v6H4z"/></svg><span>Home</span></a>
<a href="#" aria-label="Search"><svg viewBox="0 0 24 24"><circle cx="10.5" cy="10.5" r="6" fill="none" stroke="currentColor" stroke-width="2.2"/><path d="M15 15l5 5" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg><span>Search</span></a>
<a href="#" aria-label="Activity"><svg viewBox="0 0 24 24"><path d="M12 20s-7.5-4.6-7.5-10A4.3 4.3 0 0112 7.3 4.3 4.3 0 0119.5 10c0 5.4-7.5 10-7.5 10z"/></svg><span>Activity</span></a>
<a href="#" aria-current="page" aria-label="Profile"><svg viewBox="0 0 24 24"><circle cx="12" cy="8.5" r="4"/><path d="M4.5 20a7.5 7.5 0 0115 0z"/></svg><span>Profile</span></a>
</nav>
<div class="island" data-⚓️-pin-top data-⚓️-pin-x aria-hidden="true"></div>
<div class="home-indicator" data-⚓️-pin-bottom data-⚓️-pin-x aria-hidden="true"></div>
</div>
</div>
</div>
<aside class="inspector" aria-label="Constraints inspector">
<header>
<h2>Constraints</h2>
<p>Active set: <b class="sc-label">compact</b> width. Hover or focus a view to highlight it. Dimmed rows belong to the other size class.</p>
</header>
<div class="c-list">
<section data-target="--margins" tabindex="0"><h3>layoutMarginsGuide</h3><ul>
<li>margins.leading = safeArea.leading + <var class="lm">16</var></li>
<li>margins.trailing = safeArea.trailing − <var class="lm">16</var></li>
</ul></section>
<section data-target="--cover" tabindex="0"><h3>cover <small>UIImageView</small></h3><ul>
<li>cover.top = superview.top</li>
<li>cover.leading = superview.leading</li>
<li>cover.trailing = superview.trailing</li>
<li data-when="compact">cover.height = 0.333 × cover.width + safeArea.top</li>
<li data-when="regular">cover.height = 0.2 × cover.width + safeArea.top</li>
</ul></section>
<section data-target="--avatar" tabindex="0"><h3>avatar <small>UIImageView</small></h3><ul>
<li data-when="compact">avatar.centerX = superview.centerX</li>
<li data-when="regular">avatar.leading = margins.leading</li>
<li>avatar.centerY = cover.bottom</li>
<li data-when="compact">avatar.width = 0.3 × superview.width <em>@750</em></li>
<li data-when="regular">avatar.width = 0.12 × superview.width <em>@750</em></li>
<li>avatar.width ≤ 132 <em>@1000</em></li>
<li>avatar.height = 1.0 × avatar.width <em>1:1</em></li>
</ul></section>
<section data-target="--name" tabindex="0"><h3>nameLabel <small>UILabel</small></h3><ul>
<li data-when="compact">name.centerX = avatar.centerX</li>
<li data-when="compact">name.top = avatar.bottom + 8 <em>standard</em></li>
<li data-when="regular">name.leading = avatar.trailing + 16</li>
<li data-when="regular">name.top = cover.bottom + 12</li>
</ul></section>
<section data-target="--badge" tabindex="0"><h3>verifiedBadge <small>UIImageView</small></h3><ul>
<li>badge.leading = name.trailing + 4</li>
<li>badge.firstBaseline = name.firstBaseline</li>
</ul></section>
<section data-target="--handle" tabindex="0"><h3>handleLabel <small>UILabel</small></h3><ul>
<li>handle.top = name.bottom + 8 <em>standard</em></li>
<li data-when="compact">handle.centerX = name.centerX</li>
<li data-when="regular">handle.leading = name.leading</li>
</ul></section>
<section data-target="--col" tabindex="0"><h3>column <small>UILayoutGuide</small></h3><ul>
<li>column.leading = margins.leading</li>
<li data-when="compact">column.width = 1.0 × margins.width</li>
<li data-when="regular">column.width = 0.36 × margins.width</li>
</ul></section>
<section data-target="--bio" tabindex="0"><h3>bioLabel <small>UILabel, lines = 0</small></h3><ul>
<li data-when="compact">bio.top = handle.bottom + 12</li>
<li data-when="regular">bio.top = handle.bottom + 16</li>
<li>bio.leading = column.leading</li>
<li>bio.trailing = column.trailing</li>
</ul></section>
<section data-target="--stats" tabindex="0"><h3>statsStack <small>UIStackView .fillEqually</small></h3><ul>
<li>stats.top = bio.bottom + 16</li>
<li>stats.leading = column.leading</li>
<li>stats.trailing = column.trailing</li>
<li>stats.height = 60</li>
</ul></section>
<section data-target="--stat2" tabindex="0"><h3>stat[0…2] <small>arranged subviews</small></h3><ul>
<li>stat[i].width = 0.333 × stats.width</li>
<li>stat[i].top = stats.top, .bottom = stats.bottom</li>
<li>stat[0].leading = stats.leading</li>
<li>stat[i].leading = stat[i−1].trailing</li>
</ul></section>
<section data-target="--div1" tabindex="0"><h3>divider <small>hairline</small></h3><ul>
<li>divider.leading = stat[i].leading</li>
<li>divider.width = 1 ÷ displayScale</li>
<li>divider.top = stat.top + 14, .bottom = stat.bottom − 14</li>
</ul></section>
<section data-target="--follow" tabindex="0"><h3>followButton <small>UIButton</small></h3><ul>
<li>follow.top = stats.bottom + 16</li>
<li>follow.leading = column.leading</li>
<li>follow.width = 0.5 × stats.width − 4</li>
</ul></section>
<section data-target="--message" tabindex="0"><h3>messageButton <small>UIButton</small></h3><ul>
<li>message.leading = follow.trailing + 8 <em>standard</em></li>
<li>message.width = 1.0 × follow.width <em>equal</em></li>
<li>message.height = 1.0 × follow.height</li>
<li>message.top = follow.top</li>
</ul></section>
<section data-target="--grid" tabindex="0"><h3>photoGrid <small>UILayoutGuide</small></h3><ul>
<li data-when="compact">grid.top = follow.bottom + 24</li>
<li data-when="compact">grid.leading = superview.leading</li>
<li data-when="compact">grid.trailing = superview.trailing</li>
<li data-when="regular">grid.top = cover.bottom + 20</li>
<li data-when="regular">grid.leading = column.trailing + 24</li>
<li data-when="regular">grid.trailing = margins.trailing</li>
</ul></section>
<section data-target="--p5" tabindex="0"><h3>cell[0…11] <small>3 across</small></h3><ul>
<li>cell.width = 0.333 × grid.width − 1.33</li>
<li>cell.height = 1.0 × cell.width <em>1:1</em></li>
<li>cell[n].leading = cell[n−1].trailing + 2</li>
<li>cell[n+3].top = cell[n].bottom + 2</li>
</ul></section>
</div>
</aside>
</main>
</div>/* Demo styling only. Every view inside the screen is placed by data-⚓️-*
attributes (the constraints). CSS here supplies what UIKit keeps outside
the constraint graph: colours, fonts, intrinsic sizes, constraint
CONSTANTS (margins = the "+ 8" in "name.top = avatar.bottom + 8") and
MULTIPLIERS (--⚓️-mult-x), which change per size class. */
/* ---------- iOS system colours ---------- */
:root {
color-scheme: light dark;
--bg: #fff;
--grouped: #f2f2f7;
--cell-bg: #f2f2f7;
--label: #000;
--label-2: rgb(60 60 67 / 0.6);
--label-3: rgb(60 60 67 / 0.3);
--separator: rgb(60 60 67 / 0.29);
--fill: rgb(118 118 128 / 0.12);
--tint: #007aff;
--page: #e4e4ea;
--chrome: rgb(249 249 249 / 0.78);
--xray: #ff2d55;
--guide: #34c759;
--hairline: 1px;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #000;
--grouped: #000;
--cell-bg: #1c1c1e;
--label: #fff;
--label-2: rgb(235 235 245 / 0.6);
--label-3: rgb(235 235 245 / 0.3);
--separator: rgb(84 84 88 / 0.65);
--fill: rgb(118 118 128 / 0.24);
--tint: #0a84ff;
--page: #0c0c0e;
--chrome: rgb(22 22 24 / 0.75);
--xray: #ff375f;
--guide: #30d158;
}
}
@media (min-resolution: 2dppx) {
:root { --hairline: 0.5px; }
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--page);
color: var(--label);
}
/* ---------- page shell ---------- */
.page {
height: 100dvh;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 1rem;
padding: 0.6rem clamp(0.75rem, 3vw, 1.5rem);
font-size: 0.875rem;
}
.brand {
display: grid;
margin-inline-end: auto;
line-height: 1.25;
}
.brand strong { font-size: 1rem; letter-spacing: -0.01em; }
.brand span { color: var(--label-2); font-size: 0.8rem; }
code { font: 0.9em ui-monospace, "SF Mono", monospace; }
.seg {
display: flex;
margin: 0;
padding: 2px;
border: 0;
border-radius: 9px;
background: var(--fill);
}
.seg legend {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.seg label {
position: relative;
padding: 0.3rem 0.8rem;
border-radius: 7px;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s, box-shadow 0.2s;
}
.seg input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.seg label:has(:checked) {
background: var(--bg);
box-shadow: 0 3px 8px rgb(0 0 0 / 0.12), 0 0 0 0.5px rgb(0 0 0 / 0.04);
}
@media (prefers-color-scheme: dark) {
.seg label:has(:checked) { background: #636366; }
}
.seg label:has(:focus-visible),
.switch:has(:focus-visible) {
outline: 2px solid var(--tint);
outline-offset: 2px;
}
.trait {
font: 600 0.75rem ui-monospace, "SF Mono", monospace;
padding: 0.25rem 0.55rem;
border-radius: 6px;
background: var(--fill);
color: var(--label-2);
}
.trait::before { content: "horizontalSizeClass: ."; }
.switch {
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-weight: 500;
cursor: pointer;
}
.switch input {
appearance: none;
width: 42px;
height: 26px;
margin: 0;
border-radius: 99px;
background: var(--fill);
position: relative;
cursor: pointer;
transition: background-color 0.25s;
}
.switch input::after {
content: "";
position: absolute;
inset: 2px auto 2px 2px;
aspect-ratio: 1;
border-radius: 50%;
background: #fff;
box-shadow: 0 2px 5px rgb(0 0 0 / 0.2);
transition: translate 0.25s cubic-bezier(0.3, 1.4, 0.5, 1);
}
.switch input:checked { background: #34c759; }
.switch input:checked::after { translate: 16px 0; }
/* ---------- stage + device frame ---------- */
.stage {
display: grid;
grid-template-columns: minmax(0, 1fr);
min-height: 0;
}
.stage-main {
container-type: size;
display: grid;
place-items: center;
min-height: 0;
}
/* unframed (phones): the app IS the viewport */
.device {
width: 100%;
height: 100%;
overflow: clip;
view-transition-name: device;
}
.screen {
width: 100%;
height: 100%;
overflow: clip;
background: var(--bg);
color: var(--label);
/* safe area: real insets on a real device, simulated ones in the frame */
--safe-t: env(safe-area-inset-top, 0px);
--safe-b: env(safe-area-inset-bottom, 0px);
--safe-l: env(safe-area-inset-left, 0px);
--safe-r: env(safe-area-inset-right, 0px);
--lm: 16px;
timeline-scope: --page;
}
.statusbar, .island, .home-indicator { display: none; }
@media (min-width: 720px) and (min-height: 560px) {
.page:has([name="inspect"]:checked) .stage {
grid-template-columns: minmax(0, 1fr) 22rem;
}
}
/* framed: a simulated device (always on desktop; on a phone only for the
Landscape / iPad previews, zoomed down to fit) */
.page.framed .device {
--dw: 393px;
--dh: 852px;
--r: 55px;
width: min(var(--dw), 100cqw - 3rem);
height: min(var(--dh), 100cqh - 2.5rem);
border-radius: var(--r);
box-shadow:
0 0 0 11px #0d0d0f,
0 0 0 12.5px #3a3a3e,
0 40px 90px -20px rgb(0 0 0 / 0.45);
}
.page.framed[data-device="landscape"] .device { --dw: 932px; --dh: 430px; }
.page.framed[data-device="ipad"] .device { --dw: 1180px; --dh: 820px; --r: 18px; }
/* phones previewing a wider device: render it at its real logical size and
zoom the whole thing to fit, so the regular layout is the real one */
.page.framed.zoomed .device {
width: var(--dw);
height: var(--dh);
zoom: min(1, (100cqw - 2rem) / var(--dw), (100cqh - 2rem) / var(--dh));
}
.page.framed.zoomed[data-device="ipad"] .device { --dw: 820px; --dh: 1180px; }
.page.framed[data-device="portrait"] .screen { --safe-t: 59px; --safe-b: 34px; }
.page.framed[data-device="landscape"] .screen { --safe-t: 0px; --safe-b: 21px; --safe-l: 59px; --safe-r: 59px; }
.page.framed[data-device="ipad"] .screen { --safe-t: 24px; --safe-b: 20px; }
.page.framed :is(.statusbar, .home-indicator) { display: flex; }
.page.framed[data-device="landscape"] .statusbar { display: none; }
.page.framed:not([data-device="ipad"]) .island { display: block; }
/* ---------- decorative hardware (pinned, not part of the app) ---------- */
.statusbar {
z-index: 5;
height: var(--safe-t);
padding: 0 34px 0 50px;
align-items: center;
justify-content: space-between;
font-size: 17px;
font-weight: 600;
color: #fff;
pointer-events: none;
/* white over the cover photo, label colour once the bar fills */
animation: status-ink linear both;
animation-timeline: --page;
animation-range: 40px 110px;
}
@keyframes status-ink { to { color: var(--label); } }
.page[data-device="ipad"] .statusbar { padding-inline: 24px; font-size: 13px; }
.statusbar .glyphs { display: flex; gap: 6px; align-items: center; }
.statusbar svg { height: 12px; fill: currentColor; }
.statusbar svg:first-child { width: 18px; }
.statusbar svg:last-child { width: 26px; }
.island {
z-index: 6;
width: 124px;
height: 36px;
margin: 11px auto 0;
border-radius: 20px;
background: #000;
}
.page[data-device="landscape"] .island {
inset: 0 auto 0 11px;
width: 36px;
height: 124px;
margin: auto 0;
}
.home-indicator {
z-index: 6;
width: 134px;
height: 5px;
margin: 0 auto 8px;
border-radius: 3px;
background: var(--label);
pointer-events: none;
}
.page[data-device="landscape"] .home-indicator { width: 210px; }
/* ---------- scroll view ---------- */
.scroll {
height: 100%;
overflow: auto;
overscroll-behavior: contain;
scrollbar-width: none;
scroll-timeline: --page block;
outline: none;
}
.app[data-size-class="regular"] { --lm: 20px; }
/* guides: zero-height, invisible anchors */
.g {
height: 0;
pointer-events: none;
}
.g-margins {
margin-inline-start: calc(var(--safe-l) + var(--lm));
margin-inline-end: calc(var(--safe-r) + var(--lm));
}
.g-col { --⚓️-mult-x: 1; }
.app[data-size-class="regular"] .g-col { --⚓️-mult-x: 0.36; }
.g-spacer {
width: 1px;
height: calc(49px + var(--safe-b) + 24px);
}
/* ---------- content ---------- */
.cover {
/* the ratio sizes the content box; the safe-area inset rides on top as
padding, so the photo always clears the status bar */
box-sizing: content-box;
padding-block-start: var(--safe-t);
aspect-ratio: 3 / 1;
background:
radial-gradient(60% 90% at 78% 18%, rgb(255 236 210 / 0.9), transparent 60%),
linear-gradient(172deg, transparent 52%, oklch(38% 0.05 230 / 0.9) 52.5% 60%, transparent 60.5%),
linear-gradient(186deg, transparent 58%, oklch(30% 0.05 235) 58.5%),
linear-gradient(180deg, oklch(78% 0.05 240), oklch(62% 0.07 250) 55%, oklch(45% 0.06 245));
}
.app[data-size-class="regular"] .cover { aspect-ratio: 5 / 1; }
.avatar {
--⚓️-mult-x: 0.3;
max-inline-size: 132px;
aspect-ratio: 1;
min-block-size: 0; /* never let the initials' height break the 1:1 */
overflow: hidden;
display: grid;
place-items: center;
border-radius: 50%;
border: 4px solid var(--bg);
background:
radial-gradient(circle at 30% 25%, oklch(88% 0.09 60), transparent 55%),
linear-gradient(135deg, oklch(70% 0.16 30), oklch(52% 0.19 350));
color: #fff;
font-weight: 700;
font-size: clamp(1.5rem, 9cqi, 2.6rem);
letter-spacing: -0.02em;
z-index: 1;
}
.app[data-size-class="regular"] .avatar { --⚓️-mult-x: 0.12; font-size: 2.2rem; }
.name,
.badge {
margin: 0;
font-size: 22px;
line-height: 1.15;
font-weight: 700;
letter-spacing: -0.02em;
white-space: nowrap;
}
.name { margin-block-start: 8px; }
.app[data-size-class="regular"] .name {
margin-block-start: 12px;
margin-inline-start: 16px;
font-size: 26px;
}
.app[data-size-class="regular"] .badge { font-size: 26px; }
.badge {
margin-inline-start: 4px;
color: var(--tint);
}
/* baseline alignment: the badge box shares the name's line box (align-top +
sync-height), so an image sitting on the baseline lands on the name's
baseline — UIImageView's baseline is its bottom edge */
.badge svg {
width: 0.78em;
height: 0.78em;
vertical-align: baseline;
}
.handle {
margin: 8px 0 0;
font-size: 15px;
line-height: 1.15;
color: var(--label-2);
white-space: nowrap;
}
.bio {
margin: 12px 0 0;
font-size: 15px;
line-height: 1.35;
text-align: center;
text-wrap: pretty;
}
.app[data-size-class="regular"] .bio {
margin-block-start: 16px;
text-align: start;
}
.stats {
height: 60px;
margin-block-start: 16px;
border-radius: 12px;
background: var(--cell-bg);
}
.stat {
--⚓️-mult-x: calc(1 / 3);
display: grid;
place-content: center;
text-align: center;
line-height: 1.2;
}
.stat b { font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; }
.stat span { font-size: 12px; color: var(--label-2); }
.divider {
width: var(--hairline);
margin-block: 14px;
background: var(--separator);
}
.btn {
height: 36px;
border: 0;
border-radius: 10px;
font: inherit;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s, color 0.2s, scale 0.15s;
}
.btn:active { scale: 0.97; }
.btn:focus-visible {
outline: 2px solid var(--tint);
outline-offset: 2px;
}
.btn-follow {
--⚓️-mult-x: 0.5;
--⚓️-offset-x: -4px;
margin-block-start: 16px;
background: var(--tint);
color: #fff;
}
.btn-follow[aria-pressed="true"] {
background: var(--fill);
color: var(--label);
}
.btn-message {
margin-inline-start: 8px;
background: var(--fill);
color: var(--label);
}
.g-grid { margin-block-start: 24px; }
.app[data-size-class="regular"] .g-grid {
margin-block-start: 20px;
margin-inline-start: 24px;
}
.cell {
--⚓️-mult-x: calc(1 / 3);
--⚓️-offset-x: calc(-4px / 3);
aspect-ratio: 1;
background:
radial-gradient(circle at 70% 30%, oklch(92% 0.06 calc(var(--h) + 40) / 0.9) 0 9%, transparent 10%),
linear-gradient(200deg, transparent 55%, oklch(32% 0.06 var(--h) / 0.85) 55.5%),
linear-gradient(160deg, transparent 62%, oklch(42% 0.08 calc(var(--h) + 30)) 62.5%),
linear-gradient(180deg, oklch(84% 0.06 var(--h)), oklch(62% 0.1 calc(var(--h) + 20)));
}
/* the "+ 2" constants belong to the constraint the cell carries */
.cell[data-⚓️-after] { margin-inline-start: 2px; }
.cell[data-⚓️-below] { margin-block-start: 2px; }
.app[data-size-class="regular"] .cell { border-radius: 4px; }
/* ---------- bars ---------- */
.navbar {
z-index: 4;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
height: calc(var(--safe-t) + 52px);
padding: var(--safe-t) calc(var(--safe-r) + var(--lm) - 4px) 0 calc(var(--safe-l) + var(--lm) - 4px);
animation: nav-fill linear both;
animation-timeline: --page;
animation-range: 40px 110px;
}
/* only `to` keyframes + base styles = the top-of-page look, so a page too
short to scroll (inactive timeline) still renders correctly */
@keyframes nav-fill {
to {
background: var(--chrome);
-webkit-backdrop-filter: saturate(180%) blur(20px);
backdrop-filter: saturate(180%) blur(20px);
box-shadow: 0 var(--hairline) 0 var(--separator);
}
}
.nav-title {
font-size: 17px;
font-weight: 600;
opacity: 0;
translate: 0 6px;
animation: fade-in linear both;
animation-timeline: --page;
animation-range: 90px 140px;
}
@keyframes fade-in {
to { opacity: 1; translate: 0 0; }
}
.glass {
display: grid;
place-items: center;
width: 38px;
height: 38px;
padding: 0;
border: 0;
border-radius: 50%;
background: rgb(255 255 255 / 0.35);
-webkit-backdrop-filter: blur(12px) saturate(160%);
backdrop-filter: blur(12px) saturate(160%);
box-shadow: inset 0 0.5px 0 rgb(255 255 255 / 0.6), 0 2px 10px rgb(0 0 0 / 0.08);
color: var(--label);
cursor: pointer;
}
@media (prefers-color-scheme: dark) {
.glass { background: rgb(60 60 67 / 0.45); box-shadow: inset 0 0.5px 0 rgb(255 255 255 / 0.18); }
}
.glass svg { width: 22px; height: 22px; fill: currentColor; }
.glass:focus-visible { outline: 2px solid var(--tint); outline-offset: 2px; }
.tabbar {
z-index: 4;
display: flex;
justify-content: space-around;
height: calc(49px + var(--safe-b));
padding: 0 calc(var(--safe-r) + var(--lm)) var(--safe-b) calc(var(--safe-l) + var(--lm));
background: var(--chrome);
-webkit-backdrop-filter: saturate(180%) blur(20px);
backdrop-filter: saturate(180%) blur(20px);
box-shadow: 0 calc(var(--hairline) * -1) 0 var(--separator);
}
.tabbar a {
flex: 1;
display: grid;
justify-items: center;
align-content: center;
gap: 1px;
max-inline-size: 110px;
color: #999;
font-size: 10px;
font-weight: 500;
text-decoration: none;
}
.tabbar a[aria-current] { color: var(--tint); }
.tabbar svg { width: 26px; height: 26px; fill: currentColor; }
.app[data-size-class="regular"] .tabbar { justify-content: center; gap: 12px; }
.app[data-size-class="regular"] .tabbar a {
flex: 0 0 auto;
padding-inline: 14px;
grid-auto-flow: column;
gap: 6px;
font-size: 13px;
}
.app[data-size-class="regular"] .tabbar svg { width: 22px; height: 22px; }
/* ---------- constraints inspector / X-ray ---------- */
.hl {
z-index: 3;
min-height: 2px;
pointer-events: none;
opacity: 0;
outline: 2px solid var(--xray);
outline-offset: 1px;
border-radius: 4px;
background: color-mix(in srgb, var(--xray) 14%, transparent);
transition-property: inset, width, height, translate, opacity;
}
.hl::after {
content: attr(data-label);
position: absolute;
inset-block-end: 100%;
inset-inline-start: -2px;
margin-block-end: 4px;
padding: 1px 6px;
border-radius: 4px;
background: var(--xray);
color: #fff;
font: 600 11px/1.5 ui-monospace, "SF Mono", monospace;
white-space: nowrap;
}
.page.inspecting .hl.on { opacity: 1; }
/* X-ray: every anchor outlined, guides drawn as lines */
.page.inspecting .scroll [data-⚓️-name]:not(.g) {
outline: 1px dashed color-mix(in srgb, var(--xray) 70%, transparent);
outline-offset: -1px;
}
.page.inspecting .scroll .g[data-⚓️-name] {
border-block-start: 1px dashed var(--guide);
}
.page.inspecting .screen::after {
content: "";
position: absolute;
inset: 0 calc(var(--safe-r) + var(--lm)) 0 calc(var(--safe-l) + var(--lm));
z-index: 3;
border-inline: 1px dashed color-mix(in srgb, var(--guide) 75%, transparent);
pointer-events: none;
}
.inspector {
display: none;
min-height: 0;
overflow: auto;
overscroll-behavior: contain;
padding: 0 1rem 1.5rem;
background: var(--grouped);
font-size: 13px;
}
.page.inspecting .inspector { display: block; }
.inspector header {
position: sticky;
top: 0;
z-index: 1;
padding-block: 0.75rem 0.5rem;
background: var(--grouped);
}
.inspector h2 { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
.inspector header p { margin: 0.25rem 0 0; color: var(--label-2); line-height: 1.35; }
.c-list { display: grid; gap: 10px; }
.c-list section {
scroll-margin-block: 7.5rem 1rem;
border-radius: 12px;
background: var(--bg);
padding: 10px 12px;
cursor: default;
outline: none;
transition: box-shadow 0.15s;
}
@media (prefers-color-scheme: dark) {
.c-list section { background: #1c1c1e; }
}
.c-list section.active,
.c-list section:focus-visible {
box-shadow: 0 0 0 2px var(--xray);
}
.c-list h3 {
margin: 0 0 4px;
font: 600 13px ui-monospace, "SF Mono", monospace;
}
.c-list h3 small { font-weight: 400; color: var(--label-2); font-family: -apple-system, system-ui, sans-serif; }
.c-list ul {
margin: 0;
padding: 0;
list-style: none;
}
.c-list li {
padding: 3px 0;
font: 12px/1.4 ui-monospace, "SF Mono", monospace;
border-block-start: var(--hairline) solid var(--separator);
transition: opacity 0.25s;
}
.c-list li:first-child { border: 0; }
.c-list em, .c-list var { font-style: normal; }
.c-list em { color: var(--label-2); margin-inline-start: 4px; font-size: 11px; }
.c-list var { color: var(--tint); }
.page[data-sc="compact"] li[data-when="regular"],
.page[data-sc="regular"] li[data-when="compact"] {
opacity: 0.35;
text-decoration: line-through;
}
.c-list li[data-when]::after {
content: attr(data-when);
margin-inline-start: 6px;
padding: 0 5px;
border-radius: 4px;
font-size: 10px;
background: var(--fill);
color: var(--label-2);
}
/* phones: the inspector is a bottom sheet over the app */
@media not ((min-width: 720px) and (min-height: 560px)) {
.inspector {
position: fixed;
inset: auto 0 0;
z-index: 10;
max-height: 46dvh;
border-radius: 14px 14px 0 0;
box-shadow: 0 -10px 40px rgb(0 0 0 / 0.25);
}
.inspector::before {
content: "";
display: block;
width: 36px;
height: 5px;
margin: 6px auto 0;
border-radius: 3px;
background: var(--label-3);
}
.toolbar { gap: 0.5rem 0.75rem; padding-block: 0.5rem; }
.toolbar .brand { display: none; }
.trait::before { content: ""; }
}
@media (width < 480px) {
.trait { display: none; }
.toolbar { justify-content: space-between; }
}
/* ---------- size-class change: animated like a rotation ---------- */
/* each view morphs from its old frame to its new one; nested inside the
device group so nothing escapes the rounded screen while it moves */
:is(.cover, .avatar, .name, .badge, .handle, .bio, .stats, .stat, .divider, .btn, .cell, .navbar, .tabbar) {
view-transition-name: match-element;
view-transition-group: nearest;
}
::view-transition-group-children(device) {
overflow: clip;
border-radius: var(--vt-r, 0);
}
::view-transition-group(*) {
animation-duration: 0.45s;
animation-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) { animation: none !important; }
.switch input::after, .seg label, .btn { transition: none; }
}// State only. Two jobs, no position maths anywhere:
// 1. Trait changes: like traitCollectionDidChange, deactivate one constraint
// set and activate the other. Each view lists its size-class-specific
// constraints in data-compact / data-regular ("below align-x=center"), and
// activating a set is just adding/removing those data-⚓️-* attributes.
// 2. The inspector: point the highlight's data-⚓️-fill at the hovered view.
const page = document.querySelector(".page");
const app = document.querySelector(".app");
const hl = app.querySelector(".hl");
const trait = page.querySelector(".trait");
const framed = matchMedia("(min-width: 720px) and (min-height: 560px)");
const reduced = matchMedia("(prefers-reduced-motion: reduce)");
const A = "data-⚓️-";
const parse = (s = "") =>
s.split(/\s+/).filter(Boolean).map((t) => [t.split("=")[0], t.split("=")[1] ?? ""]);
function setDevice(device) {
page.dataset.device = device;
// desktop always shows a device frame; a phone is the device itself, and
// only previews Landscape / iPad as a zoomed-down frame
page.classList.toggle("framed", framed.matches || device !== "portrait");
page.classList.toggle("zoomed", !framed.matches && device !== "portrait");
}
function sizeClass() {
if (page.classList.contains("zoomed")) return "regular"; // logical width ≥ 820
return app.clientWidth >= 600 ? "regular" : "compact"; // UIKit's regular-width threshold
}
function activate(sc) {
if (app.dataset.sizeClass === sc) return;
const off = sc === "regular" ? "compact" : "regular";
for (const view of app.querySelectorAll("[data-compact]")) {
for (const [k] of parse(view.dataset[off])) view.removeAttribute(A + k);
for (const [k, v] of parse(view.dataset[sc])) view.setAttribute(A + k, v);
}
app.dataset.sizeClass = page.dataset.sc = trait.value = sc;
page.querySelector(".sc-label").textContent = sc;
}
const withTransition = (fn) =>
document.startViewTransition && !reduced.matches ? document.startViewTransition(fn) : fn();
page.querySelector(".seg").addEventListener("change", (e) => {
withTransition(() => {
setDevice(e.target.value);
activate(sizeClass());
});
});
// initial device: iPad (regular) on desktop, the phone itself on phones
if (framed.matches) page.querySelector('[value="ipad"]').checked = true;
setDevice(page.querySelector('[name="device"]:checked').value);
framed.addEventListener("change", () => setDevice(page.dataset.device));
new ResizeObserver(() => activate(sizeClass())).observe(app);
activate(sizeClass());
// ---------- inspector ----------
const inspect = page.querySelector('[name="inspect"]');
inspect.addEventListener("change", () => page.classList.toggle("inspecting", inspect.checked));
if (framed.matches && innerWidth >= 1200) inspect.click();
function focusView(section) {
page.querySelectorAll(".c-list .active").forEach((s) => s.classList.remove("active"));
if (!section) return hl.classList.remove("on");
section.classList.add("active");
const name = section.dataset.target;
const view = app.querySelector(`[data-⚓️-name="${name}"]`);
hl.setAttribute(A + "fill", name);
hl.dataset.label = view.dataset.label;
hl.classList.add("on");
return view;
}
const list = page.querySelector(".c-list");
list.addEventListener("pointerover", (e) => focusView(e.target.closest("section")));
list.addEventListener("focusin", (e) => {
focusView(e.target.closest("section"))?.scrollIntoView({ block: "nearest", behavior: reduced.matches ? "auto" : "smooth" });
});
list.addEventListener("pointerleave", () => focusView(null));
// hovering a view in the screen finds its constraints
app.querySelector(".scroll").addEventListener("pointerover", (e) => {
if (!page.classList.contains("inspecting")) return;
const view = e.target.closest("[data-⚓️-name]");
const name = view?.getAttribute(A + "name").replace(/--(stat|div)\d/, (m, k) => `--${k}${k === "stat" ? 2 : 1}`).replace(/--p\d+/, "--p5");
const section = name && list.querySelector(`[data-target="${name}"]`);
if (!section) return focusView(null);
focusView(section);
hl.setAttribute(A + "fill", view.getAttribute(A + "name"));
hl.dataset.label = view.dataset.label;
section.scrollIntoView({ block: "nearest" });
});
// a real interaction: follow toggles, the button keeps its equal-width constraint
const follow = app.querySelector(".btn-follow");
const followers = app.querySelector(".followers");
follow.addEventListener("click", () => {
const on = follow.getAttribute("aria-pressed") !== "true";
follow.setAttribute("aria-pressed", on);
follow.textContent = on ? "Following" : "Follow";
followers.textContent = on ? "12.5K" : "12.4K";
});data-⚓️-afterdata-⚓️-align-bottomdata-⚓️-align-leadingdata-⚓️-align-topdata-⚓️-align-xdata-⚓️-belowdata-⚓️-center-alldata-⚓️-filldata-⚓️-namedata-⚓️-pin-bottomdata-⚓️-pin-topdata-⚓️-pin-xdata-⚓️-rootdata-⚓️-scopedata-⚓️-stretch-xdata-⚓️-stretch-ydata-⚓️-sync-heightdata-⚓️-sync-widthdata-⚓️-todata-⚓️-transition