Library / Tutorial

Key-fob pricing,
step by step.

A pricing section where the plans are car keys on a leather pad. Press one and the car flashes its headlights, then its plan card rises beside it; press ENGINE START/STOP and every price counts to yearly. Six stages, each a live page you can run.

6Stages
3Keys
0Libraries

This is the whole build of the key-fob pricing section, in the order it was written: the plans page of a made-up car-subscription club, auvernegarage. The plans are three car keys lying on a leather desk pad, each engraved with its name and price. Press a key and the car above it flashes its headlights, then that plan's card rises beside it. The chrome ENGINE START/STOP button is the billing switch: press it and every price counts down to the yearly figure. Each stage below is the page as it stood at that step. It shows a still, and Run it here starts the live page in its place. The code under each heading is only what that stage adds. The finished source is at the end, and on GitHub under the MIT licence.

There is no framework and no build step: one HTML page, two style sheets and one ES module of about 260 lines. The photos (the car with its lamps off and on, the three fobs, the leather pad) were generated with Google Gemini and cut out by a small Python script; the engraving on the keys is live text, so the prices can change.

1 · The showroom, in HTML and CSS

Start with the scene. The right-hand side of the hero is a .rig: one box whose width sets every size inside it. With container-type: inline-size, 1cqw is one percent of the rig's width, so the car, the pad and the keys keep their places on any screen.

keyfob.css — one coordinate system
.rig {
  grid-area: rig; position: relative; justify-self: end; align-self: end;
  width: min(100%, calc((100svh - var(--hdr)) * 1000 / 760)); aspect-ratio: 1000 / 760;
  container-type: inline-size;
}

The car is two photos of the same frame, lamps off and lamps on, stacked. A mask fades the studio around it into the page: an ellipse around the car, crossed with soft left and right edges.

index.html — the car
<div class="car" aria-hidden="true"><div class="car-plate car-off"></div><div class="car-plate car-on"></div><div class="car-flare"></div></div>
keyfob.css — the car on the stage
.car {
  /* the car is the hero: ~72% of the rig wide, centred at 56%, wheels just behind the pad's far edge */
  --plate-w: calc(72cqw / var(--car-w));
  position: absolute; width: var(--plate-w); aspect-ratio: 21 / 9;
  left: calc(56cqw - var(--car-cx) * var(--plate-w));
  top: calc(33.5cqw - var(--car-floor) * var(--plate-w) * 9 / 21);
  /* fade the plate into the stage: an ellipse around the car, intersected with soft left/right edges */
  -webkit-mask-image: radial-gradient(56% 50% at calc(var(--car-cx) * 100%) 60%, #000 64%, transparent 100%), linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
  -webkit-mask-composite: source-in;
          mask-image: radial-gradient(56% 50% at calc(var(--car-cx) * 100%) 60%, #000 64%, transparent 100%), linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
          mask-composite: intersect;
  pointer-events: none;
}

The leather pad is the one image that has to work at every size. A stretched photo of leather turns into glittery asphalt, because its grain stretches with it. So the photo only gives what it is good at: its stitched rim, as a 9-slice border-image with no fill; its light, blurred and stretched (a blur can't show stretching); and a seamless tile of its grain at the photo's own scale, softened and at about 40 % of its contrast, laid over the light with overlay.

assets.css — rim, light and grain
/* the leather pad: the photo’s stitched rim as a 9-slice (no fill); in the middle, the photo’s light map stretched
   (it is blurred, so stretching cannot show) under a seamless grain tile at the photo’s own scale (never stretched) */
:root .pad { border-style: solid; border-image: image-set(url("../assets/img/leather_pad.webp") type("image/webp"), url("../assets/img/leather_pad.png") type("image/png")) 64 / calc(0.0253 * 112cqw) stretch;
  border-radius: calc(0.0170 * 112cqw); box-shadow: 0 -1.2cqw 3cqw rgba(0, 0, 0, .6);
  background: image-set(url("../assets/img/leather_grain.webp") type("image/webp"), url("../assets/img/leather_grain.png") type("image/png")) 0 0 / calc(0.3031 * 112cqw) repeat, image-set(url("../assets/img/leather_light.webp") type("image/webp"), url("../assets/img/leather_light.png") type("image/png")) 0 0 / 100% 100% no-repeat, #141312;
  background-origin: border-box; background-blend-mode: overlay, normal; }

Each key is a radio button in a radio group, with its plan name and price in the markup. The photo comes from CSS; the engraving is real text, turned to lie along the key.

index.html — one key
<div class="fob" role="radio" aria-checked="true" tabindex="0" data-plan="grand" data-finish="carbon">
  <span class="fob-body">
    <span class="fob-led" aria-hidden="true"></span>
    <span class="fob-engrave" aria-hidden="true"><span class="fob-name">Grand Tour</span><span class="fob-price"><span class="amt" data-amt="grand">$899</span>/mo</span></span>
  </span>
  <span class="sr fob-sr">Grand Tour, <span class="amt" data-amt="grand">$899</span> a month, most chosen</span>
</div>
keyfob.css — a key on the pad
.fob {
  --L: 32cqw; --x: 50%; --y: 70%;
  position: absolute; left: var(--x); top: var(--y); width: calc(var(--L) * var(--fob-ar)); height: var(--L);
  margin: calc(var(--L) * -.5) 0 0 calc(var(--L) * var(--fob-ar) / -2);
  outline: none; -webkit-tap-highlight-color: transparent;
}
keyfob.css — turned to lie diagonally
.fob-body {
  position: absolute; inset: 0; pointer-events: auto; cursor: pointer; touch-action: manipulation;
  border-radius: 40% / 18%;
  background-position: center; background-size: contain; background-repeat: no-repeat;
  transform: scale(1, .86) rotate(-125deg);
  transition: translate .35s var(--ease-out);
}
keyfob.css — the engraving
.fob-engrave {
  position: absolute; left: 50%; top: var(--engrave-y); width: 0; height: 0;
  display: grid; place-content: center; justify-items: center; white-space: nowrap; transform: rotate(90deg);
  font-family: var(--sans); line-height: 1.1; pointer-events: none;
}

One small script in the head picks the photo set before the first paint (a meta tag says which; ?fobs= overrides it). Below the hero come the compare table and the FAQ, plain HTML.

index.html — the inline script in the head
// JS on, and the fob photo set (a | b | svg), before first paint.
(function () {
  var d = document.documentElement; d.classList.remove('no-js'); d.classList.add('js');
  var q = /[?&]fobs=(a|b|svg)\b/.exec(location.search), m = document.querySelector('meta[name="dv51:fobs"]');
  d.dataset.fobs = q ? q[1] : (m && m.content) || 'b';
})();
Stage 1 · the showroomOpen ↗
The auvernegarage hero with no JavaScript: KEY-FOB pricing on the left, a black supercar with its lamps off above a stitched leather pad, and three car keys lying diagonally on it, Drive $499/mo in steel, Grand Tour $899/mo in carbon and Signature $1,499/mo in chrome.

2 · The keys are a radio group

Now the module. Choosing a key sets aria-checked on it and keeps only that key in the tab order (a roving tabindex). The compare table's column and the key ring in the compare section follow.

keyfob.js — choose a plan
function setPlan(id) {
  state.plan = id;
  el.fobs.forEach((f) => { const on = f.dataset.plan === id; f.setAttribute('aria-checked', String(on)); f.tabIndex = on ? 0 : -1; });
  const syncCompare = () => {
    if (state.plan !== id) return;
    el.colBtns.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.plan === id)));
    el.cells.forEach((c) => c.classList.toggle('is-sel', c.dataset.col === id));
    el.hangs.forEach((h) => h.classList.toggle('is-sel', h.dataset.plan === id));
  };
  syncCompare();
}

A click chooses, and so do the arrow keys (they wrap), Home and End, as the WAI-ARIA radio group pattern asks.

keyfob.js — click and keys
el.fobs.forEach((f) => { const on = f.dataset.plan === id; f.setAttribute('aria-checked', String(on)); f.tabIndex = on ? 0 : -1; });
  const syncCompare = () => {
    if (state.plan !== id) return;
    el.colBtns.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.plan === id)));
    el.cells.forEach((c) => c.classList.toggle('is-sel', c.dataset.col === id));
    el.hangs.forEach((h) => h.classList.toggle('is-sel', h.dataset.plan === id));
  };
  syncCompare();
}

el.fobs.forEach((f) => {
  f.addEventListener('click', () => { f.focus({ preventScroll: true }); setPlan(f.dataset.plan); });
  f.addEventListener('keydown', (e) => {
    const i = ORDER.indexOf(f.dataset.plan);
    let j = null;
    if (e.key === 'ArrowRight' || e.key === 'ArrowDown') j = (i + 1) % 3;
    else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') j = (i + 2) % 3;
    else if (e.key === 'Home') j = 0;
    else if (e.key === 'End') j = 2;
    else if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); setPlan(f.dataset.plan); return; }
    if (j === null) return;
    e.preventDefault();
    const n = el.fobs.find((x) => x.dataset.plan === ORDER[j]);
    n.focus(); setPlan(ORDER[j]);
  });
});

The script also switches the headlights on: the lamps-on photo simply becomes opaque.

keyfob.css — headlights on
.lights-on .car-on { opacity: 1; }
Stage 2 · a radio groupOpen ↗
The hero with the headlights on and the Signature key chosen, lifted slightly off the leather.

3 · Press a key, the car answers

Choosing a key now "unlocks" it. The car double-blinks, the way a real car answers its key: the lamps go off, on with an amber flare, off, on with a flare, on, in 760 ms. Three layers animate their opacity with the Web Animations API: the lamps-on photo, the flare on the lamps, and a glow the lamps throw onto the leather. Only opacity changes, on layers that stay composited, so no frame is ever repainted.

keyfob.js — the double blink
function blink() {   // the double blink: lamps off → on + flare → off → on + flare → on, light spilling onto the leather
  const o = { duration: 760, easing: 'linear' };
  el.carOn.animate([{ opacity: 1 }, { opacity: 0, offset: .16 }, { opacity: 1, offset: .3 }, { opacity: 0, offset: .5 }, { opacity: 1, offset: .64 }, { opacity: 1 }], o);
  el.flare.animate([{ opacity: 0 }, { opacity: 0, offset: .16 }, { opacity: 1, offset: .3 }, { opacity: 0, offset: .5 }, { opacity: 1, offset: .64 }, { opacity: 0 }], o);
  el.padGlow.animate([{ opacity: .55 }, { opacity: 0, offset: .16 }, { opacity: 1, offset: .3 }, { opacity: 0, offset: .5 }, { opacity: 1, offset: .64 }, { opacity: .55 }], o);
}
keyfob.css — the flare sits on the lamps
.car-flare {
  position: absolute; inset: 0; opacity: 0; mix-blend-mode: screen;
  background: radial-gradient(18% 16% at calc(var(--lamp-x, .5) * 100%) calc(var(--lamp-y, .6) * 100%), rgba(255, 176, 80, .7), rgba(255, 150, 60, .18) 45%, transparent 75%);
}
keyfob.js — with reduced motion, the lamps stay on
function flash() {   // a fob's unlock
  state.flashes++;
  if (reduce.matches) { el.car.classList.add('lights-on'); return; }
  blink();
}

The key itself dips, and its middle button (the open padlock) is backlit amber and pings.

keyfob.js — unlock
function unlock(id) {
  setPlan(id, { defer: true });
  const f = el.fobs.find((x) => x.dataset.plan === id);
  if (!reduce.matches) {
    f.querySelector('.fob-body').animate([{ scale: 1 }, { scale: .965, offset: .3 }, { scale: 1 }], { duration: 300, easing: 'ease-out' });
    f.querySelector('.fob-unlock').animate([{ opacity: 1, scale: .8 }, { opacity: 0, scale: 1.7 }], { duration: 600, easing: 'ease-out' });
  }
  flash();
}
keyfob.css — the unlock button
.fob-unlock { /* the open-padlock button: backlit amber on the chosen fob, pings when pressed */
  position: absolute; left: 50%; top: var(--unlock-y); width: var(--unlock-w); height: var(--unlock-h); translate: -50% -50%;
  border-radius: 42%; opacity: 0; transition: opacity .3s;
  box-shadow: 0 0 0 .15cqw rgba(255, 196, 119, .85), 0 0 1.4cqw .4cqw rgba(255, 170, 70, .55), inset 0 0 1cqw rgba(255, 170, 70, .35); }
.fob[aria-checked="true"] .fob-unlock { opacity: .7; }

The chosen plan is saved. Every storage access is wrapped, so a blocked storage (a private window) just forgets.

keyfob.js — storage that never throws
const store = {
  get(k) { try { return localStorage.getItem(k); } catch (e) { return null; } },
  set(k, v) { try { localStorage.setItem(k, v); } catch (e) { /* storage blocked: the page still works */ } },
};
Stage 3 · the headlightsOpen ↗
The car at the peak of its double blink, amber flares on both headlights and their light on the leather, the Drive key chosen with its unlock button glowing.

4 · ENGINE START/STOP switches the billing

The billing switch is a push-to-start button: a real <button aria-pressed> drawn in CSS only, a knurled chrome ring (a repeating conic gradient), a polished bezel, a brushed cap and an amber ring light.

index.html — the button
<button class="engine" type="button" aria-pressed="false" aria-describedby="billing-note" data-js-only>
  <span class="engine-knurl" aria-hidden="true"></span>
  <span class="engine-bezel" aria-hidden="true"></span>
  <span class="engine-cap" aria-hidden="true"><span class="engine-led"></span><b>ENGINE</b><small>START/STOP</small></span>
  <span class="sr">Yearly billing</span>
</button>
keyfob.css — the knurled ring
.engine-knurl {
  position: absolute; inset: 0; border-radius: 50%;
  background:
    radial-gradient(circle at 50% 50%, transparent 58%, rgba(0, 0, 0, .55) 60%, transparent 63%),
    radial-gradient(circle at 35% 25%, rgba(255, 255, 255, .45), transparent 55%),
    repeating-conic-gradient(#d8d8dc 0 1.6deg, #6e6f74 1.6deg 3.2deg);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .65), 0 0 0 1px rgba(0, 0, 0, .7), inset 0 0 0 1px rgba(255, 255, 255, .25);
}
keyfob.css — the ring light when it is on
.engine[aria-pressed="true"] .engine-led { background: var(--amber-2); box-shadow: 0 0 8px 2px rgba(255, 176, 80, .75); }

Pressing it takes 15 % off. Every price on the page follows: they count to their new figure in 620 ms, and the car answers with the same double blink.

keyfob.js — set the billing
function setBilling(b, { animate = true, save = true } = {}) {
  state.billing = b === 'yearly' ? 'yearly' : 'monthly';
  if (state.billing === 'yearly') root.dataset.billing = 'yearly'; else delete root.dataset.billing;
  el.engine.setAttribute('aria-pressed', String(state.billing === 'yearly'));
  setBillingText(); setPrices(animate);
  if (save) store.set(KEY.billing, state.billing);
  if (animate) rev();
}
keyfob.js — every price counts
function setPrices(animate) {
  const targets = $$('[data-amt]').map((n) => [n, perMonth(n.dataset.amt, state.billing)]);
  const from = targets.map(([n]) => shown.get(n) ?? parseInt(n.textContent.replace(/\D/g, ''), 10));
  if (!animate || reduce.matches) {
    targets.forEach(([n, v]) => { n.textContent = fmt(v); shown.set(n, v); });
    return;
  }
  const t0 = performance.now(), dur = 620;
  const step = (t) => {
    const k = Math.min(1, (t - t0) / dur), e = 1 - Math.pow(1 - k, 3);
    targets.forEach(([n, v], i) => { const x = Math.round(from[i] + (v - from[i]) * e); if (shown.get(n) !== x) { n.textContent = fmt(x); shown.set(n, x); } });
    if (k < 1) requestAnimationFrame(step);
  };
  requestAnimationFrame(step);
}

The choice is saved, and the head script applies it before the first paint, so a yearly visitor never sees monthly prices flash.

index.html — the head script now reads the billing too
// JS on; the fob photo set and the saved billing choice are applied before first paint.
(function () {
  var d = document.documentElement; d.classList.remove('no-js'); d.classList.add('js');
  var q = /[?&]fobs=(a|b|svg)\b/.exec(location.search), m = document.querySelector('meta[name="dv51:fobs"]');
  d.dataset.fobs = q ? q[1] : (m && m.content) || 'b';
  try {
    if (localStorage.getItem('dv51a.billing') === 'yearly') d.dataset.billing = 'yearly';
  } catch (e) {}
})();
Stage 4 · yearly billingOpen ↗
The ENGINE START/STOP button pressed with its amber ring lit, save 15% highlighted, the switch on yearly and the keys engraved $424, $764 and $1,274 a month.

5 · The plan card

Unlocking a key opens its plan's card. The card does not repeat the plans: it reads its features from the compare table, the single source.

keyfob.js — the features, from the table
const FEATS = Object.fromEntries(ORDER.map((id) => [id, [...document.querySelectorAll('.cmp tbody tr[data-card]')].map((tr) => {
  const td = tr.querySelector(`td[data-col="${id}"]`);
  if (td.querySelector('.no')) return null;
  return [tr.querySelector('th').textContent.trim(), td.querySelector('.yes') ? 'Included' : td.textContent.trim()];
}).filter(Boolean).slice(0, 5)]));
keyfob.js — unlock now opens the card
function unlock(id, { focus = false } = {}) {
  const changed = id !== state.plan;
  setPlan(id, { defer: true });
  const f = el.fobs.find((x) => x.dataset.plan === id);
  if (!reduce.matches) {
    f.querySelector('.fob-body').animate([{ scale: 1 }, { scale: .965, offset: .3 }, { scale: 1 }], { duration: 300, easing: 'ease-out' });
    f.querySelector('.fob-unlock').animate([{ opacity: 1, scale: .8 }, { opacity: 0, scale: 1.7 }], { duration: 600, easing: 'ease-out' });
  }
  flash();
  if (changed && state.open) rise();
  openCard({ focus });
}

Where it opens matters. Over the car it would hide the very flash the press just caused, so on wide screens it rises in the intro column, over the headline, out of a clip line just above the ENGINE button. On a short screen it drops its kicker and two features; only if it still does not fit does it reach over the button. All the reads come before the writes, so opening it never forces a layout.

keyfob.js — place the card
function placeCard() {
  const side = state.open && !SHEET.matches;
  if (!side) {
    el.stage.classList.remove('card-side'); el.card.classList.remove('compact');
    el.clip.style.removeProperty('--cx'); el.clip.style.removeProperty('--cb'); el.clip.classList.remove('over'); return;
  }
  // every read first, then the writes: no forced layout in the unlock path
  const rr = el.stage.getBoundingClientRect(), ir = el.intro.getBoundingClientRect(), er = el.engine.getBoundingClientRect();
  const hb = el.top.getBoundingClientRect().bottom;
  const w = el.card.offsetWidth || 340, full = el.card.offsetHeight || 360;
  // short screens: a compact card (no kicker, three features) when the full one would reach the engine
  const wasCompact = el.card.classList.contains('compact');
  if (!wasCompact) placeCard.extra = [$('.card-kicker', el.card), ...$$('.card-feats li', el.card).slice(3)].reduce((n, e) => n + e.offsetHeight + 5, 0);
  const extra = placeCard.extra || 0;
  const cx = ir.left - rr.left + w / 2;
  let bottom = rr.bottom - (er.top - 14);
  const topLimit = Math.max(12, hb - rr.top + 12);
  const room = rr.height - bottom - topLimit;
  const fullH = wasCompact ? full + extra : full, compact = fullH > room;
  el.card.classList.toggle('compact', compact);
  const h = compact === wasCompact ? full : el.card.offsetHeight;   // re-measured only when the mode changed (short screens)
  const over = h > room;                                     // still too tall: the card may cover the engine's top
  if (over) bottom = rr.height - h - topLimit;
  el.clip.style.setProperty('--cx', cx + 'px');
  el.clip.style.setProperty('--cb', bottom + 'px');
  el.clip.classList.toggle('over', over);
  el.stage.classList.add('card-side');
}
keyfob.css — the clip it rises out of
.card-clip {
  --w: clamp(280px, 31vw, 380px);
  position: absolute; z-index: 6; left: var(--cx, 50%); bottom: var(--cb, 40%); width: var(--w); margin-left: calc(var(--w) / -2);
  overflow: hidden; padding: 24px 24px 0; margin-bottom: 0; translate: -24px 0; box-sizing: content-box; pointer-events: none;
}
keyfob.js — the rise
function rise() {
  if (reduce.matches) el.card.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200 });
  else el.card.animate([{ transform: 'translateY(105%)' }, { transform: 'none' }], { duration: 520, easing: 'cubic-bezier(.2, .8, .2, 1)' });
}
keyfob.css — the headline steps aside
.stage.card-side h1 { opacity: 0; }

On phones, portrait tablets and short screens it is a bottom sheet.

keyfob.css — the bottom sheet
/* the card becomes a bottom sheet where there is no room above the fobs */
@media (max-width: 719.98px), (max-height: 560px), (max-aspect-ratio: 6/5) {
  .card-clip { position: fixed; z-index: 55; left: 0; right: 0; bottom: 0; width: auto; margin: 0; translate: none; padding: 30px 0 0; }
  .card { border-radius: 22px 22px 0 0; padding: 18px 20px calc(16px + env(safe-area-inset-bottom)); max-height: calc(100svh - var(--hdr) - 30px); overflow: auto; overscroll-behavior: contain; }
  .card-clip { pointer-events: none; }
Stage 5 · the plan cardOpen ↗
The Grand Tour card risen in the intro column: Subscription details, $899/mo, billed monthly, five features and Subscribe to Grand Tour, with the car and all three keys still in view.

6 · The phone, and the finish

A phone has no room for the navigation, so the header becomes a menu button, the wordmark and a key button that opens the chosen plan's sheet.

index.html — the phone header
<button class="menu-btn" type="button" aria-expanded="false" aria-controls="menu" data-js-only>
  <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h10"/></svg><span class="sr">Menu</span>
</button>
<button class="bag-btn" type="button" aria-controls="plan-card" data-js-only>
  <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 14.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Zm2.4-1.3H20l-1.5 1.6-1.4-1.1-1.3 1.2-1.3-1.2-1.3 1.2-1.5-1.2"/></svg><span class="sr">Your key: </span><span class="bag-plan">Grand Tour</span>
keyfob.js — the menu
function setMenu(on) { el.menuBtn.setAttribute('aria-expanded', String(on)); el.menu.hidden = !on; }
el.menuBtn.addEventListener('click', () => setMenu(el.menu.hidden));
$$('#menu a').forEach((a) => a.addEventListener('click', () => setMenu(false)));
document.addEventListener('keydown', (e) => {
  if (e.key !== 'Escape') return;
  if (!el.menu.hidden) { setMenu(false); el.menuBtn.focus(); } else if (state.open) closeCard();
});
document.addEventListener('pointerdown', (e) => { if (!el.menu.hidden && !el.menu.contains(e.target) && !el.menuBtn.contains(e.target)) setMenu(false); });

Log in is not part of a pricing demo, so it says so politely.

keyfob.js — a toast
function toast(msg) {
  el.toast.textContent = msg; el.toast.classList.add('is-on');
  clearTimeout(toast.t); toast.t = setTimeout(() => el.toast.classList.remove('is-on'), 2800);
}

Last, the saved keys are versioned (dv51a.*), and anything older is wiped before it can confuse the page.

index.html — the final head script
// JS on; the billing choice is applied before first paint. Keys are versioned (dv51a.*); anything older is wiped.
(function () {
  var d = document.documentElement; d.classList.remove('no-js'); d.classList.add('js');
  var q = /[?&]fobs=(a|b|svg)\b/.exec(location.search), m = document.querySelector('meta[name="dv51:fobs"]');
  d.dataset.fobs = q ? q[1] : (m && m.content) || 'b';
  try {
    for (var i = localStorage.length - 1; i >= 0; i--) { var k = localStorage.key(i); if (/^dv51/.test(k) && !/^dv51a\./.test(k)) localStorage.removeItem(k); }
    if (localStorage.getItem('dv51a.billing') === 'yearly') d.dataset.billing = 'yearly';
  } catch (e) {}
})();

Measured with the project's frame probe on a 120 Hz screen through presses, blinks, the billing switch, the card and the compare view: no long tasks and no missed frames at 1440 × 900, on a 390 × 844 phone, or with the CPU slowed 4 ×.

Stage 6 · the finished sectionOpen ↗
The finished section: yearly billing on, the Signature card in the intro column at $1,274/mo, the car with its headlights on and the three keys on the leather.
Stage 6 · on a phoneOpen ↗
The section on a phone: the car, the three keys on the leather, yearly billing on, and the Grand Tour plan as a bottom sheet at $764 a month.

Make it yours

  • Your plans are PLANS in keyfob.js (name and monthly price) and the compare table in the HTML; the card reads its five features from the table rows marked data-card.
  • Your photos: drop a car with its lamps off and on (the same frame), a sheet of three keys and a leather pad into assets/incoming/ and run scripts/ingest.py. It finds the car and its lamps, cuts the keys out and writes assets.css; the page code does not change.
  • The yearly discount is YEARLY_OFF.
  • Accessibility is already there: a real radio group with roving tabindex, a toggle button with aria-pressed, a real table with captions and headers, focus that returns to the key when the card closes, 44 px targets, and prefers-reduced-motion honoured.

The code is on GitHub at UlviShabanbayli/domvybe-keyfob-pricing, under the MIT licence: use it in your own projects, commercial ones included. auvernegarage is made up. The car and key fob photos and the leather were generated with Google Gemini; everything else was made for this project.

The one idea worth keeping

Let the page answer the press where the eye already is. The key is pressed, the car answers with its lamps, and the card opens where it hides nothing: three reactions to one press, and not one of them covers another.