AI Library

Mobile UI design

Whole phone interfaces rebuilt as working replicas — the design language read off a video and put back together screen by screen, with the numbers it runs on, the source file, and a prompt that reproduces it. Tap through them; they are not pictures.

Mind Space: an ambient AI phone OS

A working replica of the whole design language: a sky that takes the hue of whichever context is on top, frosted context cards that carry third-party widgets inside them, and a prompt bar that is the only way to talk to it. Three screens, all tappable.

  • Sky hsl(--h 55% 16%) → 62% 44% @32% → 50% 80% @62% → #f5f4f9 @84%
  • Hues Family 275 · Trip 190 · Work 228
  • Hue transition 900ms cubic-bezier(.22,.61,.36,1) via @property
Code
mind-space-ai-os.html
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mind Space — an ambient AI phone OS</title>
<style>
  /* Animate the ambient hue as a NUMBER. Without @property a custom property
     flips instantly and the whole sky changes in one frame. */
  @property --h { syntax: "<number>"; inherits: true; initial-value: 275; }

  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { height: 100%; }
  body {
    font-family: "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
    background: #07081a; color: #17151f; overflow: hidden;
    -webkit-font-smoothing: antialiased; -webkit-user-select: none; user-select: none;
  }
  /* ---------- the stage: dark room, blue ribbons, as in the source render ---------- */
  .stage { position: absolute; inset: 0; display: grid; place-items: center; overflow: hidden; }
  .stage::before {
    content: ""; position: absolute; inset: -20%;
    background:
      repeating-linear-gradient(112deg, #0b1a6e00 0 90px, #1330b8 110px 150px, #0b1a6e00 170px 260px),
      radial-gradient(70% 60% at 70% 30%, #1b2fa8 0%, #07081a 70%);
    filter: blur(28px); opacity: .55;
  }

  /* ---------- the device ---------- */
  .phone {
    position: relative; width: 360px; height: 740px;
    transform: scale(var(--s, 1)); transform-origin: 50% 50%;
    border-radius: 46px; background: #0c0c12; padding: 12px;
    box-shadow: 0 40px 80px -30px #000c, 0 0 0 1px #2a2a36, inset 0 0 0 2px #1a1a24;
  }
  .screen {
    position: relative; width: 100%; height: 100%; border-radius: 36px; overflow: hidden;
    --h: 275;
    transition: --h 900ms cubic-bezier(.22,.61,.36,1);
    /* Ambient: one hue, four stops. Dark saturated at the top, dissolving to
       paper by 78%. Every screen state only ever changes --h and --dark. */
    background: linear-gradient(180deg,
      hsl(var(--h) 55% 16%) 0%,
      hsl(var(--h) 62% 44%) 32%,
      hsl(var(--h) 50% 80%) 62%,
      #f5f4f9 84%);
    font-size: 12px; line-height: 1.35;
  }
  .screen::after {                      /* punch-hole camera */
    content: ""; position: absolute; left: 50%; top: 10px; width: 12px; height: 12px;
    margin-left: -6px; border-radius: 50%; background: #000; box-shadow: inset 0 0 0 2px #14141c; z-index: 30;
  }
  .status {
    position: absolute; left: 0; right: 0; top: 0; height: 34px; padding: 10px 20px 0;
    display: flex; justify-content: space-between; align-items: center;
    font-size: 11.5px; font-weight: 600; color: var(--ink, #fff); z-index: 20;
    transition: color 500ms;
  }
  .status i { display: inline-block; width: 12px; height: 8px; margin-left: 5px; background: currentColor; border-radius: 2px; opacity: .9; }
  .status i.sig { width: 10px; mask: linear-gradient(90deg, #000 0 20%, #0000 20% 30%, #000 30% 50%, #0000 50% 60%, #000 60% 80%, #0000 80% 90%, #000 90%); -webkit-mask: linear-gradient(90deg, #000 0 20%, #0000 20% 30%, #000 30% 50%, #0000 50% 60%, #000 60% 80%, #0000 80% 90%, #000 90%); }
  .status i.wifi { border-radius: 50% 50% 2px 2px; height: 9px; }
  .status i.bat { width: 18px; border-radius: 3px; box-shadow: inset 0 0 0 1.5px currentColor, inset 0 0 0 3px var(--ink-bg, #0000); }
  .home-ind { position: absolute; left: 50%; bottom: 8px; width: 110px; height: 4px; margin-left: -55px; border-radius: 2px; background: #17151f66; z-index: 20; }

  /* ---------- shared atoms ---------- */
  .chip {
    display: inline-flex; align-items: center; height: 20px; padding: 0 9px; border-radius: 99px;
    font-size: 10px; font-weight: 500; letter-spacing: .01em;
    border: 1px solid #17151f2a; background: #ffffff8c; color: #17151f;
  }
  .dark .chip { border-color: #ffffff59; background: #ffffff24; color: #fff; }
  .glass {
    background: #ffffffa8; border: 1px solid #ffffffb0; border-radius: 28px;
    backdrop-filter: blur(22px) saturate(1.2); -webkit-backdrop-filter: blur(22px) saturate(1.2);
    box-shadow: 0 12px 30px -18px #1a123a55;
  }
  .glass.dark { background: #ffffff1f; border-color: #ffffff38; color: #fff; box-shadow: none; }

  section { position: absolute; inset: 0; }
  .home { padding: 44px 14px 0; }

  /* tabs: one bordered pill for the current context, the rest are just words */
  .tabs { display: flex; gap: 6px; align-items: center; overflow: hidden; white-space: nowrap; padding: 4px 2px; }
  .tab {
    height: 30px; padding: 0 13px; border-radius: 99px; border: 1px solid transparent;
    font-size: 12px; font-weight: 500; color: #ffffffb8; background: none; cursor: pointer;
    transition: border-color 400ms, background-color 400ms, color 400ms;
  }
  .tab.on { border-color: #ffffff8c; background: #ffffff1f; color: #fff; }

  /* ---------- the card stack ---------- */
  .stack { position: relative; margin-top: 10px; display: flex; flex-direction: column; gap: 10px; }
  .card {
    position: relative; padding: 14px 14px 14px 16px; min-height: 172px;
    display: grid; grid-template-columns: 1fr 138px; gap: 10px; cursor: pointer;
    transition: transform 620ms cubic-bezier(.22,.61,.36,1), background-color 600ms, border-color 600ms, color 600ms;
    will-change: transform;
  }
  .card.moving { transition: none; }
  .card h3 { margin-top: 10px; font-size: 15.5px; font-weight: 600; letter-spacing: -.01em; }
  .card p { margin-top: 3px; font-size: 11px; color: #17151fa6; max-width: 150px; }
  .dark.card p { color: #ffffffb3; }
  .card .foot { position: absolute; left: 16px; bottom: 14px; display: flex; align-items: center; gap: 6px; font-size: 10.5px; font-weight: 600; }
  .card .foot small { font-weight: 400; opacity: .6; }
  .ring { width: 18px; height: 18px; border-radius: 50%; background: conic-gradient(currentColor calc(var(--v) * 1%), #17151f22 0); mask: radial-gradient(circle, #0000 55%, #000 56%); -webkit-mask: radial-gradient(circle, #0000 55%, #000 56%); }
  .tick { width: 14px; height: 14px; border-radius: 50%; background: currentColor; position: relative; }
  .tick::after { content: ""; position: absolute; left: 4px; top: 3px; width: 4px; height: 7px; border: solid #fff; border-width: 0 1.8px 1.8px 0; transform: rotate(45deg); }
  .dark .tick::after { border-color: #17151f; }

  /* nested widget: a white inset card; the third-party thing lives here */
  .widget {
    border-radius: 20px; background: #ffffffe0; color: #17151f; padding: 11px 12px; position: relative;
    box-shadow: 0 8px 20px -12px #1a123a4d; overflow: hidden;
  }
  .dark .widget { background: #ffffffe6; }
  .widget b { display: block; font-size: 11px; line-height: 1.3; }
  .widget .mark { position: absolute; right: 10px; top: 10px; width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center; font-size: 10px; font-weight: 800; color: #fff; }
  .collage { position: relative; height: 62px; margin-top: 8px; }
  .collage i {
    position: absolute; width: 60px; height: 52px; border-radius: 14px; border: 3px solid #fff; box-shadow: 0 6px 14px -6px #0006;
  }
  .collage i:first-child { left: 8px; top: 6px; transform: rotate(-8deg); background: linear-gradient(160deg, #cfe9e7, #5fa9a3 55%, #2f6f6b); }
  .collage i:last-child { left: 52px; top: 2px; transform: rotate(6deg); background: radial-gradient(circle at 45% 40%, #fff2c2 0 14%, #f0b25a 15% 30%, #a2561e 31% 60%, #5a2e12 61%); }
  .timeline { margin-top: 8px; position: relative; padding-left: 14px; font-size: 10px; }
  .timeline::before { content: ""; position: absolute; left: 4px; top: 6px; bottom: 6px; border-left: 1px solid #17151f33; }
  .timeline div { position: relative; margin-bottom: 8px; }
  .timeline div::before { content: ""; position: absolute; left: -13px; top: 4px; width: 6px; height: 6px; border-radius: 50%; border: 1.5px solid #17151f; background: #fff; }
  .timeline b { font-size: 15px; font-weight: 600; display: block; line-height: 1.1; }
  .timeline .gate { font-size: 10px; }
  .timeline .gate span { display: inline-block; margin-left: 4px; padding: 1px 6px; border-radius: 6px; background: #17151f; color: #fff; font-weight: 600; }
  .sources { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; margin-top: -2px; }
  .src { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; color: #fff; font-size: 10px; font-weight: 800; box-shadow: 0 4px 10px -4px #0006; }
  .avs { display: flex; align-items: center; gap: 6px; padding: 4px 10px 4px 4px; border-radius: 99px; background: #ffffffd9; color: #17151f; font-size: 10px; font-weight: 600; }
  .avs i { width: 18px; height: 18px; border-radius: 50%; margin-right: -8px; border: 2px solid #fff; }
  .count { position: absolute; right: 14px; top: 16px; font-size: 10px; color: inherit; opacity: .75; display: flex; align-items: center; gap: 6px; }
  .count b { display: grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; background: #17151f14; font-size: 10px; opacity: 1; }
  .dark .count b { background: #ffffff2e; }

  /* the floating prompt: sits over the stack, never inside it */
  .prompt {
    position: absolute; left: 22px; right: 22px; bottom: 24px; height: 40px; border-radius: 99px;
    display: flex; align-items: center; padding: 0 16px; font-size: 12.5px; color: #17151f80;
    background: #ffffffe6; border: 1px solid #ffffff; box-shadow: 0 10px 30px -10px #1a123a55; cursor: text;
    backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  }
  .prompt::after { content: ""; width: 1px; height: 14px; background: #17151f; margin-left: 2px; animation: blink 1s steps(2) infinite; }
  @keyframes blink { to { opacity: 0; } }

  /* ---------- detail ---------- */
  .detail, .agents { opacity: 0; pointer-events: none; transform: translateY(28px); transition: opacity 380ms, transform 480ms cubic-bezier(.22,.61,.36,1); }
  .detail.open, .agents.open { opacity: 1; pointer-events: auto; transform: none; }
  .detail {
    padding: 46px 18px 0; color: #fff; overflow: hidden;
    /* same hue, the sky pulled lower: the hero is dark, the lists are paper */
    background: linear-gradient(180deg, hsl(var(--h) 55% 16%) 0%, hsl(var(--h) 60% 40%) 30%, hsl(var(--h) 45% 82%) 52%, #f5f4f9 66%);
  }
  .home { transition: opacity 300ms; }
  .screen.away .home { opacity: 0; pointer-events: none; }
  .detail h1 { margin-top: 10px; font-size: 24px; font-weight: 600; letter-spacing: -.02em; }
  .detail .lede { margin-top: 6px; font-size: 11.5px; color: #ffffffcc; max-width: 250px; }
  .detail h4 { margin-top: 18px; font-size: 12px; font-weight: 600; }
  .srcs { margin-top: 8px; position: relative; }
  .srcs .row { display: flex; align-items: center; gap: 8px; height: 30px; font-size: 11px; font-weight: 500; }
  .srcs .row .src { width: 20px; height: 20px; font-size: 8px; }
  .srcs .row em { font-style: normal; opacity: .5; font-size: 10px; margin-left: 2px; }
  .srcs .when { position: absolute; right: 0; top: 0; text-align: right; font-size: 10px; color: #ffffff8c; }
  .srcs .when div { height: 30px; line-height: 30px; }
  .srcs .more { text-align: center; font-size: 10px; color: #ffffffb0; margin-top: 2px; }
  .nextup { display: flex; gap: 8px; margin-top: 8px; overflow: hidden; }
  .mini { flex: 0 0 170px; padding: 12px; border-radius: 18px; background: #ffffffd9; color: #17151f; }
  .mini b { display: block; font-size: 12px; }
  .mini span { display: block; margin-top: 3px; font-size: 10px; color: #17151f99; }
  .actions { margin-top: 8px; }
  .actions .mini { flex: none; background: #ffffffe0; }
  .actions .mini b { background: linear-gradient(90deg, #e2427a, #b23dd9); -webkit-background-clip: text; background-clip: text; color: transparent; }
  .detail h4.paper, .detail .paper { color: #17151f; }
  .bar {
    position: absolute; left: 14px; right: 14px; bottom: 22px; height: 40px; display: flex; align-items: center; gap: 6px;
    padding: 0 6px; border-radius: 99px; background: #ffffffe6; border: 1px solid #fff; color: #17151f; overflow: hidden; white-space: nowrap;
    box-shadow: 0 10px 30px -10px #1a123a55;
  }
  .bar .ic { width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; background: #17151f0f; flex: none; cursor: pointer; }
  .bar .t { padding: 0 12px; height: 28px; line-height: 28px; border-radius: 99px; font-size: 11px; font-weight: 500; color: #17151f99; }
  .bar .t.on { background: #17151f; color: #fff; }
  .back { position: absolute; left: 16px; top: 44px; width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; cursor: pointer; z-index: 25; background: #ffffff2e; color: #fff; font-size: 14px; }

  /* ---------- agents / follow-up ---------- */
  .agents { background: #f5f4f9; color: #17151f; padding: 46px 16px 0; }
  .agents .back { background: #17151f10; color: #17151f; }
  .thumb {
    position: absolute; right: 16px; top: 40px; width: 150px; height: 74px; border-radius: 14px; padding: 10px 12px;
    color: #fff; overflow: hidden; box-shadow: 0 10px 24px -12px #000a;
    background: linear-gradient(160deg, hsl(var(--h) 55% 18%), hsl(var(--h) 62% 40%));
  }
  .thumb .chip { transform: scale(.85); transform-origin: 0 0; }
  .thumb b { display: block; font-size: 11px; margin-top: 4px; }
  .thumb span { font-size: 8px; opacity: .7; }
  .agents .tabs { margin-top: 84px; padding: 0; }
  .agents .tab { color: #17151f99; height: 28px; font-size: 11.5px; }
  .agents .tab.on { border-color: #17151f26; background: #fff; color: #17151f; box-shadow: 0 4px 12px -8px #0006; }
  .agents .lede { margin-top: 12px; font-size: 11px; color: #17151f99; }
  .agent { display: grid; grid-template-columns: 54px 1fr; gap: 10px; align-items: center; margin-top: 8px; padding: 10px; border-radius: 18px; background: #fff; box-shadow: 0 8px 24px -16px #1a123a55; }
  .agent .note { height: 60px; border-radius: 8px; background: #f3f2f6; padding: 6px; font-size: 5px; line-height: 1.3; color: #17151f99; overflow: hidden; }
  .agent .note b { display: block; font-size: 6px; color: #17151f; }
  .agent .note i { display: block; width: 90%; height: 3px; margin-top: 3px; border-radius: 2px; background: #17151f1a; }
  .agent h5 { font-size: 12px; font-weight: 600; }
  .agent span { display: block; font-size: 10px; color: #17151f99; margin-top: 2px; }
  .follow { margin-top: 12px; display: flex; align-items: center; gap: 8px; height: 40px; padding: 0 12px; border-radius: 14px; background: #fff; box-shadow: 0 8px 24px -16px #1a123a55; font-size: 12.5px; color: #17151f80; }
  .follow .plus { font-size: 18px; color: #17151f; }
  .follow .mic { margin-left: auto; width: 14px; height: 18px; border-radius: 7px; border: 2px solid #17151f; }
  .suggest { display: flex; gap: 6px; margin-top: 8px; }
  .suggest span { padding: 5px 10px; border-radius: 99px; background: #fff; font-size: 10.5px; color: #17151f; }
  .kbd { position: absolute; left: 0; right: 0; bottom: 0; padding: 8px 4px 22px; background: #d9d9e2; }
  .kbd .r { display: flex; justify-content: center; gap: 4px; margin-bottom: 6px; }
  .kbd .k { width: 30px; height: 36px; border-radius: 6px; background: #fff; box-shadow: 0 1px 0 #8a8a99; display: grid; place-items: center; font-size: 15px; font-weight: 500; }
  .kbd .k.w { width: 42px; background: #b8b8c6; font-size: 10px; }
  .kbd .k.sp { width: 150px; font-size: 11px; }

  .hint { position: absolute; left: 0; right: 0; top: 7px; text-align: center; font-size: 10px; letter-spacing: .18em; text-transform: uppercase; color: #ffffff55; }
  @media (prefers-reduced-motion: reduce) {
    .screen, .card, .detail, .agents, .tab { transition: none; }
    .prompt::after { animation: none; }
    .hint { display: none; }
  }
</style>

<div class="stage">
  <div class="phone" id="phone">
    <div class="screen" id="screen">
      <div class="status"><span>19:30</span><span><i class="sig"></i><i class="wifi"></i><i class="bat"></i></span></div>

      <section class="home">
        <nav class="tabs" id="tabs"></nav>
        <div class="stack" id="stack"></div>
        <div class="prompt" id="prompt">I want to become&nbsp;</div>
      </section>

      <section class="detail" id="detail">
        <div class="back" data-go="home">‹</div>
        <div class="count" style="right:18px;top:50px;color:#fff"><span id="d-countlabel">Tasks to-do</span><b id="d-count">6</b></div>
        <div style="margin-top:6px;padding-left:40px"><span class="chip" id="d-chip">Work</span></div>
        <h1 id="d-title">Build Brain.ai</h1>
        <p class="lede" id="d-lede"></p>
        <h4 id="d-srclabel">Based on 8 sources</h4>
        <div class="srcs" id="d-srcs"></div>
        <h4 class="paper">Next up</h4>
        <div class="nextup" id="d-next"></div>
        <h4 class="paper">AI Actions</h4>
        <div class="nextup actions" id="d-actions"></div>
        <div class="bar">
          <span class="ic" data-go="agents">💬</span><span class="t on">AI Answer</span><span class="t">Notes</span><span class="t">Market &amp; Competitors</span>
        </div>
      </section>

      <section class="agents" id="agents">
        <div class="back" data-go="home">‹</div>
        <div class="thumb"><span class="chip" id="a-chip">Work</span><b id="a-title">Build Brain.ai</b><span>Based on 8 sources</span></div>
        <nav class="tabs"><span class="tab on">AI Agents</span><span class="tab">Notes</span><span class="tab">Market &amp; Competitors</span></nav>
        <p class="lede">Specialized agents ready to help with “<span id="a-title2">Build Brain.ai</span>”</p>
        <div id="a-list"></div>
        <div class="follow"><span class="plus">+</span>Follow up<span class="mic"></span></div>
        <div class="suggest"><span>Suggest</span><span>Suggest</span><span>Suggest</span></div>
        <div class="kbd" id="kbd"></div>
      </section>

      <div class="home-ind"></div>
    </div>
  </div>
  <div class="hint">tap a tab · a card · the prompt</div>
</div>

<script>
  /* ---------- data ----------
     Three "contexts". Each carries its own hue: the ambient sky is not a
     theme, it is whichever context sits on top right now. */
  const CARDS = {
    relax: {
      hue: 275, chip: "Family", title: "Relaxation Ideas", sub: "Activities for Couples",
      foot: `<span class="tick"></span>Completed <small>2 of 5</small>`,
      widget: `<b>Spa, Herbal<br>Tea Ritual<br>and more</b><span class="mark" style="background:#ff385c">⌂</span><div class="collage"><i></i><i></i></div>`,
      lede: "A weekend of small rituals for two — a spa afternoon, an evening tea ceremony, and one surprise.",
      count: ["Ideas saved", 5], srcLabel: "Based on 3 sources",
      sources: [["Stays", "#ff385c", "Yesterday"], ["Calendar", "#2f6fff", "Today"], ["Maps", "#22a06b", "Recent searches"]],
      next: [["Book the spa slot", "Saturday 14:00 still open"], ["Tea ritual kit", "Arrives Thursday"]],
      actions: [["Find a quiet café nearby", "Walking distance from the spa"]],
      agents: [["Weekend Planner", "Builds the itinerary around both calendars"], ["Gift Scout", "Finds the one surprise that fits"]],
    },
    trip: {
      hue: 190, chip: "Family", title: "Spring Trip", sub: "Complete vacation planning",
      foot: `<span class="ring" style="--v:45"></span>45% <small>2 of 5</small>`,
      widget: `<b>SA2847<br><span style="font-weight:400">to London</span></b><span class="mark" style="background:#ffe600;color:#000;font-size:7px">spirit</span>
        <div class="timeline"><div><b>2:00</b>SFO</div><div class="gate">Gate<span>B26</span></div><div><b>4:30</b>LHR</div></div>`,
      lede: "Six days in London for four. Flights are booked; the hotel, the museum day and the trains are not.",
      count: ["Tasks to-do", 3], srcLabel: "Based on 5 sources",
      sources: [["Mail", "#2f6fff", "Yesterday"], ["Wallet", "#111", "Today"], ["Maps", "#22a06b", "Recent searches"]],
      next: [["Confirm the hotel", "Two options under budget"], ["Museum day", "Tickets release Monday"]],
      actions: [["Compare train vs. car to Bath", "Time, cost, kids"]],
      agents: [["Itinerary Agent", "Keeps the days balanced for the kids"], ["Budget Watcher", "Flags anything over the trip cap"]],
    },
    brain: {
      hue: 228, chip: "Work", title: "Build Brain.ai", sub: "Lead the mission to reinvent personal computing by creating the next generation of AI-native phones.",
      foot: ``,
      widget: null,
      lede: "Lead the mission to reinvent personal computing by creating the next generation of AI-native phones.",
      count: ["Tasks to-do", 6], srcLabel: "Based on 8 sources",
      sources: [["Line", "#06c755", "Yesterday"], ["Calendar", "#2f6fff", "Today"], ["Chrome", "#e64a3b", "Recent searches"]],
      next: [["Define Product Vision", "Clearly articulate the core features and unique selling pro…"], ["Outline Tech Stack", "Develop a high-level architecture"]],
      actions: [["Research Competitor Offerings", "Clearly articulate the core features"]],
      agents: [["Define Product Vision", "Clearly articulate the core features"], ["Keynote for Investors", "Narrative, numbers and the demo"]],
    },
  };
  // which context each tab puts on top — the sky follows that card
  const TABS = [["Mind Space", ["relax", "trip", "brain"]], ["Personal", ["trip", "relax", "brain"]], ["Family", ["relax", "brain", "trip"]], ["Work", ["brain", "relax", "trip"]]];

  const $ = (id) => document.getElementById(id);
  const screen = $("screen"), stack = $("stack"), tabs = $("tabs");
  const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;

  // ---------- home: cards
  const cardEls = {};
  for (const [id, c] of Object.entries(CARDS)) {
    const el = document.createElement("article");
    el.className = "card glass"; el.dataset.id = id;
    const brain = id === "brain";
    el.innerHTML = `
      <div><span class="chip">${c.chip}</span><h3>${c.title}</h3><p${brain ? ' style="max-width:100%;grid-column:1/-1"' : ""}>${c.sub}</p></div>
      ${brain ? `<div class="count"><span>Tasks to-do</span><b>6</b></div>
        <div class="sources"><span class="src" style="background:#06c755">L</span><span class="src" style="background:#2f6fff">▦</span><span class="src" style="background:conic-gradient(#e64a3b 0 33%,#ffcd2b 0 66%,#22a06b 0)"><i style="width:10px;height:10px;border-radius:50%;background:#fff;display:block"></i></span>
        <span class="avs"><i style="background:#f0b25a"></i><i style="background:#5fa9a3"></i><i style="background:#c9789a"></i><span style="margin-left:12px">+5 sources</span></span></div>`
      : `<div class="widget">${c.widget}</div>`}
      <div class="foot">${c.foot}</div>`;
    if (brain) el.style.gridTemplateColumns = "1fr";
    el.addEventListener("click", () => openDetail(id));
    cardEls[id] = el;
  }

  // ---------- home: tabs
  let tabIndex = 0, auto = 0;
  TABS.forEach(([name], i) => {
    const b = document.createElement("button");
    b.className = "tab"; b.textContent = name;
    b.addEventListener("click", () => { clearInterval(auto); setTab(i); });
    tabs.appendChild(b);
  });

  /* Reorder with FLIP: measure, move the DOM, measure again, then let each
     card travel from its old spot to its new one. The list is the truth; the
     animation is derived from it, so nothing can end up out of sync. */
  function setTab(i) {
    tabIndex = i;
    [...tabs.children].forEach((b, k) => b.classList.toggle("on", k === i));
    const order = TABS[i][1];
    const before = {};
    for (const id of order) before[id] = cardEls[id].getBoundingClientRect().top;
    for (const id of order) stack.appendChild(cardEls[id]);
    order.forEach((id, k) => {
      const el = cardEls[id];
      el.classList.toggle("dark", k === 0);            // top card sits on the dark sky
      if (reduce || before[id] === undefined) return;
      const dy = before[id] - el.getBoundingClientRect().top;
      el.classList.add("moving");
      el.style.transform = `translateY(${dy / scale}px)`;
      el.getBoundingClientRect();                     // flush, then release
      el.classList.remove("moving");
      el.style.transform = "";
    });
    screen.style.setProperty("--h", CARDS[order[0]].hue);
  }

  // ---------- detail
  let current = "brain";
  function openDetail(id) {
    clearInterval(auto); current = id;
    const c = CARDS[id];
    screen.style.setProperty("--h", c.hue);
    $("d-chip").textContent = c.chip; $("d-title").textContent = c.title; $("d-lede").textContent = c.lede;
    $("d-countlabel").textContent = c.count[0]; $("d-count").textContent = c.count[1]; $("d-srclabel").textContent = c.srcLabel;
    $("d-srcs").innerHTML = c.sources.map(([n, col]) => `<div class="row"><span class="src" style="background:${col}">${n[0]}</span>${n}<em>·</em></div>`).join("")
      + `<div class="when">${c.sources.map(([, , w]) => `<div>${w}</div>`).join("")}</div><div class="more">+5 sources</div>`;
    $("d-next").innerHTML = c.next.map(([b, s]) => `<div class="mini"><b>${b}</b><span>${s}</span></div>`).join("");
    $("d-actions").innerHTML = c.actions.map(([b, s]) => `<div class="mini"><b>${b}</b><span>${s}</span></div>`).join("");
    go("detail");
  }
  function openAgents() {
    const c = CARDS[current];
    $("a-chip").textContent = c.chip; $("a-title").textContent = c.title; $("a-title2").textContent = c.title;
    $("a-list").innerHTML = c.agents.map(([b, s]) => `<div class="agent"><div class="note"><b>Michael Chen</b>12:20 PM<i></i><i></i><i></i></div><div><h5>${b}</h5><span>${s}</span></div></div>`).join("");
    go("agents");
  }
  function go(where) {
    $("detail").classList.toggle("open", where === "detail");
    screen.classList.toggle("away", where !== "home");
    $("agents").classList.toggle("open", where === "agents");
    document.querySelector(".status").style.setProperty("--ink", where === "agents" ? "#17151f" : "#fff");
    if (where === "home") screen.style.setProperty("--h", CARDS[TABS[tabIndex][1][0]].hue);
  }
  document.querySelectorAll("[data-go]").forEach((el) => el.addEventListener("click", (e) => { e.stopPropagation(); el.dataset.go === "agents" ? openAgents() : go(el.dataset.go); }));
  $("prompt").addEventListener("click", () => { clearInterval(auto); openAgents(); });

  // keyboard
  const rows = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"];
  $("kbd").innerHTML = rows.map((r, i) => `<div class="r">${i === 2 ? '<span class="k w">⇧</span>' : ""}${[...r].map((k) => `<span class="k">${k}</span>`).join("")}${i === 2 ? '<span class="k w">⌫</span>' : ""}</div>`).join("")
    + `<div class="r"><span class="k w">?123</span><span class="k">,</span><span class="k sp">English</span><span class="k">.</span><span class="k w">⏎</span></div>`;

  // ---------- fit the device to the frame
  let scale = 1;
  function fit() {
    scale = Math.min(1, (innerHeight - 36) / 740, (innerWidth - 24) / 360);
    $("phone").style.setProperty("--s", scale.toFixed(4));
  }
  addEventListener("resize", fit); fit();

  // ---------- start: the showcase walks the tabs until someone touches it
  setTab(0);
  if (!reduce) auto = setInterval(() => setTab((tabIndex + 1) % TABS.length), 3600);
</script>
Prompt
Build a self-contained mobile UI replica in vanilla HTML/CSS/JS: a 360×740 phone (12px black bezel, radius 46 outside / 36 screen, 12px punch-hole camera, status bar '19:30') scaled to fit the viewport with transform: scale(min(1, (innerHeight−36)/740, (innerWidth−24)/360)). The screen background is ONE gradient driven by a registered custom property: @property --h { syntax: '<number>'; inherits: true; initial-value: 275 } and background: linear-gradient(180deg, hsl(var(--h) 55% 16%) 0%, hsl(var(--h) 62% 44%) 32%, hsl(var(--h) 50% 80%) 62%, #f5f4f9 84%) with transition: --h 900ms cubic-bezier(.22,.61,.36,1). Home: a row of pill tabs (Mind Space / Personal / Family / Work; the active one has a 1px #ffffff8c border and #ffffff1f fill) and a vertical stack of three context cards, each a 28px-radius frosted panel (#ffffffa8, 1px #ffffffb0 border, backdrop-filter blur(22px)) with a chip (20px pill), a 15.5px/600 title, an 11px subtitle, a progress footer (check + 'Completed 2 of 5', a conic ring + '45% 2 of 5', or 'Tasks to-do 6'), and a right-hand inset white widget (radius 20, #ffffffe0) holding a third-party item: a photo collage with a red circular mark, a flight timeline 'SA2847 to London · 2:00 SFO · Gate B26 · 4:30 LHR' with a yellow mark, or a row of source-app circles plus an avatar pile '+5 sources'. Each context carries a hue (275, 190, 228). Each tab defines an order of the three cards; on tab change reorder the DOM with FLIP (measure tops, appendChild in the new order, measure again, set translateY(delta) with transitions off, flush with getBoundingClientRect, then remove the transform so a 620ms cubic-bezier(.22,.61,.36,1) transition carries it), give the first card a dark variant (#ffffff1f, white text) and write its hue into --h. A 40px floating prompt pill 'I want to become' with a blinking caret sits over the stack. Tapping a card opens a detail screen (translateY 28px → 0, 480ms) using the same hue with the gradient pulled lower (paper from 66%): back button, chip, 24px title, lede, 'Based on 8 sources' rows (Line / Calendar / Chrome with 'Yesterday / Today / Recent searches' right-aligned, '+5 sources' centred), 'Next up' and 'AI Actions' as 170px white minis (radius 18), and a bottom bar with a chat icon and pill tabs 'AI Answer / Notes / Market & Competitors'. Tapping the prompt or the chat icon opens the agents screen on paper: the context shrinks to a 150×74 dark thumbnail top-right, tabs 'AI Agents / Notes / Market & Competitors', 'Specialized agents ready to help with …', agent cards with a 54px note thumbnail, a 'Follow up' field with + and mic, three 'Suggest' chips, and a QWERTY keyboard with an 'English' space bar. Hide the home screen (opacity 0) while another screen is open. Auto-advance the tabs every 3.6s until the first interaction. Under prefers-reduced-motion remove all transitions and the auto-advance.

Ambient hue follows focus

The background is not a theme, it is a number. Whatever has focus hands over its hue, one registered custom property tweens to it, and the whole screen changes mood without any element being recoloured.

  • Sky stops 16% / 44% / 80% lightness · 55 / 62 / 50 saturation · paper @90%
  • Transition --h 900ms cubic-bezier(.22,.61,.36,1)
  • Hues 275 · 190 · 228
Code
ambient-hue-follows-focus.html
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ambient hue follows focus</title>
<style>
  /* 🩸 Register the hue as a <number>. An unregistered custom property has no
     interpolation type, so `transition: --h` is ignored and the sky snaps. */
  @property --h { syntax: "<number>"; inherits: true; initial-value: 275; }

  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { height: 100%; }
  body {
    font-family: "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
    --h: 275;
    transition: --h 900ms cubic-bezier(.22,.61,.36,1);
    /* The whole ambience is this one gradient. Nothing else is coloured. */
    background: linear-gradient(180deg,
      hsl(var(--h) 55% 16%) 0%,
      hsl(var(--h) 62% 44%) 34%,
      hsl(var(--h) 50% 80%) 66%,
      #f5f4f9 90%);
    color: #17151f; overflow: hidden; -webkit-font-smoothing: antialiased;
  }
  .wrap { height: 100%; display: grid; grid-template-rows: auto 1fr auto; padding: 22px 24px 18px; }
  .top { display: flex; justify-content: space-between; align-items: baseline; color: #fff; }
  .top h1 { font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
  .top .read { font: 500 11px/1 ui-monospace, Menlo, monospace; color: #ffffffb0; }
  .top .read b { color: #fff; font-weight: 600; }

  .row { display: flex; gap: 12px; align-items: center; justify-content: center; }
  .card {
    width: 190px; height: 210px; padding: 16px; border-radius: 26px; cursor: pointer;
    background: #ffffff59; border: 1px solid #ffffff80; color: #17151f;
    backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
    display: flex; flex-direction: column; gap: 8px;
    transition: background-color 500ms, transform 500ms cubic-bezier(.22,.61,.36,1), box-shadow 500ms;
  }
  .card:hover, .card.on { background: #ffffffe0; color: #17151f; transform: translateY(-6px); box-shadow: 0 24px 40px -24px #1a123a80; }
  .chip { align-self: flex-start; padding: 3px 9px; border-radius: 99px; font-size: 10px; font-weight: 500; border: 1px solid currentColor; opacity: .8; }
  .card h3 { margin-top: auto; font-size: 17px; font-weight: 600; letter-spacing: -.01em; }
  .card p { font-size: 11px; opacity: .7; }
  /* the swatch: each card carries its hue as data and shows it as a dot */
  .card i { width: 16px; height: 16px; border-radius: 50%; background: hsl(var(--c) 62% 44%); box-shadow: 0 0 0 3px #ffffff80; }

  .foot { text-align: center; font-size: 10px; letter-spacing: .18em; text-transform: uppercase; color: #17151f80; }
  @media (prefers-reduced-motion: reduce) { body, .card { transition: none; } .foot { display: none; } }
</style>

<div class="wrap">
  <div class="top"><h1>Mind Space</h1><div class="read">--h <b id="rh">275</b></div></div>
  <div class="row" id="row">
    <div class="card on" data-h="275"><span class="chip">Family</span><i style="--c:275"></i><h3>Relaxation Ideas</h3><p>Activities for couples</p></div>
    <div class="card" data-h="190"><span class="chip">Family</span><i style="--c:190"></i><h3>Spring Trip</h3><p>Complete vacation planning</p></div>
    <div class="card" data-h="228"><span class="chip">Work</span><i style="--c:228"></i><h3>Build Brain.ai</h3><p>Tasks to-do · 6</p></div>
  </div>
  <div class="foot">hover a card · the sky follows it</div>
</div>

<script>
  /* The page never picks a colour. It asks the focused thing what hue it
     carries and writes that ONE number; the gradient does the rest. */
  const cards = [...document.querySelectorAll(".card")];
  const rh = document.getElementById("rh");
  const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
  function focus(card) {
    cards.forEach((c) => c.classList.toggle("on", c === card));
    document.body.style.setProperty("--h", card.dataset.h);
  }
  cards.forEach((c) => { c.addEventListener("pointerenter", () => focus(c)); c.addEventListener("click", () => focus(c)); });
  // readout shows the interpolated value, which is what proves it animates
  (function loop() {
    rh.textContent = Math.round(parseFloat(getComputedStyle(document.body).getPropertyValue("--h")));
    if (!reduce) requestAnimationFrame(loop);
  })();
</script>
Prompt
Build an 'ambient hue follows focus' panel in vanilla HTML/CSS/JS. Register the hue: @property --h { syntax: '<number>'; inherits: true; initial-value: 275 }. On body set --h: 275, transition: --h 900ms cubic-bezier(.22,.61,.36,1), and background: linear-gradient(180deg, hsl(var(--h) 55% 16%) 0%, hsl(var(--h) 62% 44%) 34%, hsl(var(--h) 50% 80%) 66%, #f5f4f9 90%). Place three 190×210 cards (radius 26, #ffffff59 with a 1px #ffffff80 border and backdrop-filter blur(20px)), each with a data-h attribute (275, 190, 228), a chip, a colour dot showing hsl(var(--c) 62% 44%), a title and a subtitle. On pointerenter and click, mark that card focused (background #ffffffe0, translateY(-6px), 500ms) and write its data-h into --h on the body. Show a monospace readout in the corner that reads the computed --h every animation frame and prints it rounded, so the interpolation is visible. Never set any colour on any element from JavaScript except that one number. Under prefers-reduced-motion drop the transitions.

Glass stack FLIP reorder

Switching a filter tab re-sorts a stack of cards: each card slides from where it was to where it now is, the one that lands on top turns dark, and the sky takes its hue. The DOM order is the truth; the motion is derived from it.

  • Travel 620ms cubic-bezier(.22,.61,.36,1) on transform
  • Invert .moving { transition: none } + one getBoundingClientRect() flush
  • Top card #ffffff1f · white type · 600ms colour transition
Code
glass-stack-flip-reorder.html
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Glass stack FLIP reorder</title>
<style>
  @property --h { syntax: "<number>"; inherits: true; initial-value: 275; }
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { height: 100%; }
  body {
    font-family: "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
    --h: 275; transition: --h 900ms cubic-bezier(.22,.61,.36,1);
    background: linear-gradient(180deg, hsl(var(--h) 55% 16%) 0%, hsl(var(--h) 62% 44%) 34%, hsl(var(--h) 50% 80%) 66%, #f5f4f9 90%);
    color: #17151f; overflow: hidden; -webkit-font-smoothing: antialiased;
  }
  .wrap { height: 100%; display: grid; grid-template-columns: 1fr 250px; gap: 28px; padding: 22px 26px; align-items: start; }
  .tabs { display: flex; gap: 6px; }
  .tab { height: 30px; padding: 0 13px; border-radius: 99px; border: 1px solid transparent; font-size: 12px; font-weight: 500; color: #ffffffb8; background: none; cursor: pointer; transition: border-color 400ms, background-color 400ms, color 400ms; }
  .tab.on { border-color: #ffffff8c; background: #ffffff1f; color: #fff; }

  .stack { margin-top: 12px; display: flex; flex-direction: column; gap: 10px; max-width: 360px; }
  .card {
    height: 96px; padding: 14px 16px; border-radius: 22px; position: relative;
    background: #ffffffa8; border: 1px solid #ffffffb0;
    backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px);
    /* FLIP: the transform is the animation. `moving` switches it off for one
       frame so the inverted position lands without tweening. */
    transition: transform 620ms cubic-bezier(.22,.61,.36,1), background-color 600ms, border-color 600ms, color 600ms;
    will-change: transform;
  }
  .card.moving { transition: none; }
  .card.dark { background: #ffffff1f; border-color: #ffffff38; color: #fff; }
  .chip { display: inline-block; padding: 2px 9px; border-radius: 99px; font-size: 10px; font-weight: 500; border: 1px solid #17151f2a; background: #ffffff8c; }
  .dark .chip { border-color: #ffffff59; background: #ffffff24; color: #fff; }
  .card h3 { margin-top: 8px; font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
  .card p { font-size: 11px; opacity: .65; margin-top: 2px; }
  .card i { position: absolute; right: 16px; top: 16px; width: 14px; height: 14px; border-radius: 50%; background: hsl(var(--c) 62% 44%); box-shadow: 0 0 0 3px #ffffff80; }

  .log {
    margin-top: 42px; padding: 14px 16px; border-radius: 16px; background: #ffffff8c; border: 1px solid #ffffffb0;
    font: 500 10.5px/1.7 ui-monospace, Menlo, monospace; color: #17151fb0; white-space: pre; min-height: 150px;
  }
  .log b { color: #17151f; }
  .foot { position: absolute; left: 0; right: 0; bottom: 10px; text-align: center; font-size: 10px; letter-spacing: .18em; text-transform: uppercase; color: #17151f80; }
  @media (prefers-reduced-motion: reduce) { body, .card, .tab { transition: none; } .foot { display: none; } }
</style>

<div class="wrap">
  <div>
    <nav class="tabs" id="tabs"></nav>
    <div class="stack" id="stack"></div>
  </div>
  <div class="log" id="log"></div>
</div>
<div class="foot">tap a tab · the stack re-sorts, the sky follows the top card</div>

<script>
  const CARDS = {
    relax: { h: 275, chip: "Family", t: "Relaxation Ideas", s: "Activities for couples" },
    trip: { h: 190, chip: "Family", t: "Spring Trip", s: "Complete vacation planning" },
    brain: { h: 228, chip: "Work", t: "Build Brain.ai", s: "Tasks to-do · 6" },
  };
  const TABS = [["Mind Space", ["relax", "trip", "brain"]], ["Personal", ["trip", "relax", "brain"]], ["Family", ["relax", "brain", "trip"]], ["Work", ["brain", "relax", "trip"]]];
  const stack = document.getElementById("stack"), tabs = document.getElementById("tabs"), log = document.getElementById("log");
  const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
  const els = {};
  for (const [id, c] of Object.entries(CARDS)) {
    const el = document.createElement("article"); el.className = "card"; el.dataset.id = id;
    el.innerHTML = `<span class="chip">${c.chip}</span><i style="--c:${c.h}"></i><h3>${c.t}</h3><p>${c.s}</p>`;
    els[id] = el;
  }
  let cur = 0, auto = 0;
  TABS.forEach(([n], i) => { const b = document.createElement("button"); b.className = "tab"; b.textContent = n; b.onclick = () => { clearInterval(auto); setTab(i); }; tabs.appendChild(b); });

  /* FLIP = First, Last, Invert, Play.
     First: measure where every card is. Last: move the DOM to the new order
     and measure again. Invert: transform each card back to where it was.
     Play: drop the transform and let the CSS transition carry it home.
     The DOM order is the truth; the motion is derived from it. */
  function setTab(i) {
    cur = i;
    [...tabs.children].forEach((b, k) => b.classList.toggle("on", k === i));
    const order = TABS[i][1];
    const first = Object.fromEntries(order.map((id) => [id, els[id].getBoundingClientRect().top]));
    for (const id of order) stack.appendChild(els[id]);                     // Last
    const lines = [];
    order.forEach((id, k) => {
      const el = els[id]; el.classList.toggle("dark", k === 0);
      const dy = first[id] - el.getBoundingClientRect().top;                // Invert
      lines.push(`${id.padEnd(6)} Δy ${String(Math.round(dy)).padStart(5)}px`);
      if (reduce || !dy) return;
      el.classList.add("moving"); el.style.transform = `translateY(${dy}px)`;
      el.getBoundingClientRect();                                            // flush
      el.classList.remove("moving"); el.style.transform = "";                // Play
    });
    document.body.style.setProperty("--h", CARDS[order[0]].h);
    log.innerHTML = `<b>${TABS[i][0]}</b>\n${lines.join("\n")}\n--h → <b>${CARDS[order[0]].h}</b>`;
  }
  setTab(0);
  if (!reduce) auto = setInterval(() => setTab((cur + 1) % TABS.length), 3200);
</script>
Prompt
Build a FLIP-reordered card stack in vanilla HTML/CSS/JS. Three 96px-tall frosted cards (radius 22, #ffffffa8, 1px #ffffffb0 border, backdrop-filter blur(22px)) in a flex column with 10px gap; each has a chip, a title, a subtitle, a colour dot, and a hue (275, 190, 228). Four pill tabs each define an order of the three cards. On tab change: record each card's getBoundingClientRect().top (First); appendChild the cards in the new order (Last); for each card compute dy = firstTop − newTop, add a class that sets transition: none, set transform: translateY(dy px), call getBoundingClientRect() once to flush layout, then remove the class and clear the transform so the stylesheet's transition: transform 620ms cubic-bezier(.22,.61,.36,1) plays (Invert, Play). Skip cards whose dy is 0. After reordering, give the first card a dark variant (#ffffff1f, white text, 600ms colour transitions) and set a registered custom property --h (@property, syntax '<number>', transition 900ms) on the body to that card's hue; the body background is linear-gradient(180deg, hsl(var(--h) 55% 16%), hsl(var(--h) 62% 44%) 34%, hsl(var(--h) 50% 80%) 66%, #f5f4f9 90%). Print each card's Δy and the new --h in a monospace panel. Auto-cycle the tabs every 3.2s until a tab is clicked. Under prefers-reduced-motion skip the transforms.
All categories