Mobile UI design

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.

How it works

Numbers

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
Card
#ffffffa8 · blur 22px · r 28 · dark variant #ffffff1f
Widget / mini
#ffffffe0 r 20 · #ffffffd9 r 18
Re-sort
FLIP 620ms cubic-bezier(.22,.61,.36,1)
Type
24 / 15.5 / 12.5 / 11 / 10 · 600 & 500
Device
360×740 · bezel 12 · r 46/36 · punch-hole 12

Prompt

Paste this into any coding model to get the effect from scratch. It states every number explicitly — vague words like “smooth” or “modern” produce a different result every time.

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.

Source

The whole file. It is what the sample above is running — copy it into an .html file and it works with nothing else.

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>

Where it breaks

Seen in

@海森堡 (Douyin) · “自然人工智能设计探索的思维空间” · 0:00–0:19