Back to the library

Edit lease — contested

Variant: Contested

Two edit requests for an unlocked file, 18ms apart, and the photo finish that settles them. The requests enter the log 64ms apart in arrival order, timestamps in tabular figures so the deciding digits line up. Then the winner’s avatar rises out of its row into the lock’s empty seat — a shared element, not a new one appearing, so the eye follows the request to the lock it took — on 320ms of ease-in-out, because it is already on screen travelling from A to B. Everything the grant decides waits for the landing: the shackle shuts, the lease is issued on ease-out, the label names the holder. The loser does not shake or flash. Being second is not an error, it is a place in line, so the row steps back to 55% and reads "queued" — that one word set in full ink, since muted type inside a dimmed row stops being legible. Use it anywhere a claim can race: seat holds, ticket assignment, collaborative locks.

statuspresencelistloop

The source

A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Edit lease — contested</title>
    <style>
      :root {
        /* Neutrals — fixed, never remixed. */
        --ivory: #faf9f5;
        --paper: #ffffff;
        --oat: #e3dacc;
        --slate: #141413;

        /* Knobs — the remix API. A knob exists iff declared here and used below. */
        --hue: 38.8;
        --lift: 0;
        --chroma: 1;
        --round: 1;
        --speed: 1;

        /* Accents — derived from the knobs with the family's offsets baked in,
           so one hue roll moves them together. Defaults reproduce the original hex. */
        --clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
        --clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
        --olive: oklch(
          calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3)
        );
        --rust: oklch(
          calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3)
        );

        --_muted: color-mix(in srgb, var(--slate) 50%, transparent);
        --_hair: color-mix(in srgb, var(--slate) 9%, transparent);
        --_ink: color-mix(in srgb, var(--slate) 72%, transparent);

        /* Clock — 6.4s: two requests stamped 18ms apart (shown entering 64ms
           apart, in arrival order), the first granted at 20–25%, its holder
           riding the lease until the re-arm at 90%. */
        --cycle: 6.4s;
        --t: calc(var(--cycle) / var(--speed));

        --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
        --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);

        /* Hold-row centre to first log-row centre: 16 + 8 + 4 + 14 + 14. Every
           height on that path is a fixed px below, so this holds at any width. */
        --_rise: -56px;
      }

      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }

      body {
        min-height: 100vh;
        display: grid;
        place-items: center;
        background: var(--ivory);
        font-family:
          system-ui,
          -apple-system,
          'Segoe UI',
          Roboto,
          sans-serif;
        color: var(--slate);
      }

      .card {
        width: min(300px, calc(100vw - 32px));
        padding: clamp(14px, 4.5vw, 18px);
        border-radius: calc(14px * var(--round));
        background: var(--paper);
        box-shadow:
          0 0 0 1px var(--_hair),
          0 1px 2px var(--_hair);
      }

      .doc {
        display: flex;
        align-items: center;
        gap: 7px;
        padding-bottom: 12px;
        margin-bottom: 12px;
        border-bottom: 1px solid var(--_hair);
        font-size: 12.5px;
        font-weight: 500;
        letter-spacing: -0.005em;
      }

      .doc svg {
        flex: none;
        width: 14px;
        height: 14px;
        fill: none;
        stroke: var(--_muted);
        stroke-width: 1.4;
        stroke-linejoin: round;
      }

      .stage {
        animation: rearm var(--t) var(--ease-out) infinite;
      }

      .hold {
        display: flex;
        align-items: center;
        gap: 8px;
        height: 32px;
      }

      .lock {
        flex: none;
        width: 16px;
        height: 16px;
        fill: none;
        stroke: var(--_ink);
        stroke-width: 1.6;
        stroke-linecap: round;
      }

      .lock rect {
        fill: var(--_ink);
        stroke: none;
      }

      .open {
        animation: open var(--t) infinite;
      }

      .shut {
        opacity: 0;
        animation: shut var(--t) infinite;
      }

      .slot {
        position: relative;
        flex: none;
        width: 24px;
        height: 24px;
      }

      .empty {
        position: absolute;
        inset: 0;
        border-radius: 50%;
        border: 1.5px dashed color-mix(in srgb, var(--slate) 26%, transparent);
      }

      .avatar {
        position: absolute;
        inset: 0;
        display: grid;
        place-items: center;
        border-radius: 50%;
        background: color-mix(in srgb, var(--_c) 18%, var(--paper));
        box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--_c) 34%, transparent);
        font-size: 11px;
        font-weight: 600;
        color: color-mix(in srgb, var(--_c) 70%, var(--slate));
      }

      .jun {
        --_c: var(--olive);
      }

      .you {
        --_c: var(--clay);
      }

      /* Only used when motion is reduced: the winner appears in the seat
         instead of travelling to it. */
      .seated {
        opacity: 0;
      }

      .runner {
        z-index: 1;
        animation: grant var(--t) infinite;
      }

      .stack {
        flex: 1;
        min-width: 0;
        display: grid;
        font-size: 13px;
        font-weight: 500;
        letter-spacing: -0.01em;
      }

      .stack span {
        grid-area: 1 / 1;
        white-space: nowrap;
      }

      .avail {
        color: var(--_muted);
        animation: was-free var(--t) var(--ease-out) infinite;
      }

      .taken {
        opacity: 0;
        animation: now-held var(--t) var(--ease-out) infinite;
      }

      .lease {
        position: relative;
        height: 4px;
        margin-top: 8px;
        border-radius: 999px;
        background: var(--oat);
      }

      .fill {
        position: absolute;
        inset: 0;
        border-radius: 999px;
        background: var(--olive);
        transform-origin: left center;
        transform: scaleX(0);
        animation: issue var(--t) infinite;
      }

      .log {
        display: grid;
        gap: 4px;
        margin-top: 14px;
        list-style: none;
      }

      .log li {
        display: flex;
        align-items: center;
        gap: 8px;
        height: 28px;
        font-size: 12.5px;
        opacity: 0;
      }

      .log li:nth-child(1) {
        animation: arrive-1 var(--t) var(--ease-out) infinite;
      }

      .log li:nth-child(2) {
        animation: arrive-2 var(--t) var(--ease-out) infinite;
      }

      .n {
        flex: none;
        width: 16px;
        text-align: center;
        font-size: 11px;
        color: var(--_muted);
        font-variant-numeric: tabular-nums;
      }

      .ghost {
        position: absolute;
        inset: 0;
        border-radius: 50%;
        border: 1.5px dashed color-mix(in srgb, var(--slate) 18%, transparent);
      }

      .name {
        flex: 1;
        font-weight: 500;
      }

      .at {
        font-size: 11.5px;
        color: var(--_muted);
        font-variant-numeric: tabular-nums;
      }

      .verdict {
        display: grid;
        width: 66px;
        font-size: 11.5px;
        text-align: right;
      }

      .verdict span {
        grid-area: 1 / 1;
      }

      .asked {
        color: var(--_muted);
        animation: was-asked var(--t) var(--ease-out) infinite;
      }

      .won,
      .wait {
        opacity: 0;
        animation: ruled var(--t) var(--ease-out) infinite;
      }

      .won {
        color: color-mix(in srgb, var(--olive) 78%, var(--slate));
        font-weight: 500;
      }

      /* Full ink inside a row already stepped back to 55% — muted on muted
         would leave the one word the loser needs barely legible. */
      .wait {
        color: var(--_ink);
      }

      /* The loser does not shake or flash: being second is not an error, it is
         a place in line. It steps back to 55% and says so. */
      .loser {
        display: flex;
        align-items: center;
        gap: 8px;
        flex: 1;
        min-width: 0;
        animation: step-back var(--t) var(--ease-out) infinite;
      }

      @keyframes arrive-1 {
        0%,
        8% {
          opacity: 0;
          transform: translateY(4px);
        }
        11.5%,
        94% {
          opacity: 1;
          transform: translateY(0);
        }
        94.5%,
        100% {
          opacity: 0;
          transform: translateY(4px);
        }
      }

      @keyframes arrive-2 {
        0%,
        9% {
          opacity: 0;
          transform: translateY(4px);
        }
        12.5%,
        94% {
          opacity: 1;
          transform: translateY(0);
        }
        94.5%,
        100% {
          opacity: 0;
          transform: translateY(4px);
        }
      }

      /* The winner's avatar is the element that moves into the seat — a shared
         element, not a new one appearing — so the eye follows the request to
         the lock it took. 320ms on ease-in-out: it is already on screen, going
         from A to B. */
      @keyframes grant {
        0%,
        20% {
          transform: translateY(0);
          animation-timing-function: var(--ease-in-out);
        }
        25%,
        94% {
          transform: translateY(var(--_rise));
        }
        94.5%,
        100% {
          transform: translateY(0);
        }
      }

      /* Everything that the grant decides happens on the frame the avatar
         lands, never before it: the lock shuts, the lease is issued, the label
         names the holder. */
      @keyframes open {
        0%,
        25% {
          opacity: 1;
        }
        25.25%,
        94% {
          opacity: 0;
        }
        94.5%,
        100% {
          opacity: 1;
        }
      }

      @keyframes shut {
        0%,
        25% {
          opacity: 0;
        }
        25.25%,
        94% {
          opacity: 1;
        }
        94.5%,
        100% {
          opacity: 0;
        }
      }

      @keyframes issue {
        0%,
        25% {
          transform: scaleX(0);
          animation-timing-function: var(--ease-out);
        }
        30% {
          transform: scaleX(1);
          animation-timing-function: linear;
        }
        90%,
        94% {
          transform: scaleX(0.72);
        }
        94.5%,
        100% {
          transform: scaleX(0);
        }
      }

      @keyframes was-free {
        0%,
        25% {
          opacity: 1;
        }
        26.5%,
        94% {
          opacity: 0;
        }
        94.5%,
        100% {
          opacity: 1;
        }
      }

      @keyframes now-held {
        0%,
        26.5% {
          opacity: 0;
        }
        29.5%,
        94% {
          opacity: 1;
        }
        94.5%,
        100% {
          opacity: 0;
        }
      }

      @keyframes was-asked {
        0%,
        25% {
          opacity: 1;
        }
        26.5%,
        94% {
          opacity: 0;
        }
        94.5%,
        100% {
          opacity: 1;
        }
      }

      @keyframes ruled {
        0%,
        26.5% {
          opacity: 0;
        }
        29.5%,
        94% {
          opacity: 1;
        }
        94.5%,
        100% {
          opacity: 0;
        }
      }

      @keyframes step-back {
        0%,
        25% {
          opacity: 1;
        }
        29.5%,
        94% {
          opacity: 0.55;
        }
        94.5%,
        100% {
          opacity: 1;
        }
      }

      /* Re-armed in the dark: everything snaps to its first frame at 94–94.5%
         while the stage is out. */
      @keyframes rearm {
        0%,
        90% {
          opacity: 1;
        }
        93%,
        95.5% {
          opacity: 0;
        }
        99.5%,
        100% {
          opacity: 1;
        }
      }

      @media (prefers-reduced-motion: reduce) {
        /* Nothing travels: the requests fade in where they sit, and the winner
           fades out of its row as it fades into the seat. The bar stays — it is
           the lease. One shared 1.6 keeps every cue on the grant frame. */
        .stage,
        .open,
        .shut,
        .avail,
        .taken,
        .fill,
        .asked,
        .won,
        .wait,
        .loser {
          animation-duration: calc(var(--t) * 1.6);
        }

        .log li:nth-child(1) {
          animation: arrive-still-1 calc(var(--t) * 1.6) var(--ease-out) infinite;
        }

        .log li:nth-child(2) {
          animation: arrive-still-2 calc(var(--t) * 1.6) var(--ease-out) infinite;
        }

        .runner {
          animation: leave-row calc(var(--t) * 1.6) var(--ease-out) infinite;
        }

        .seated {
          animation: take-seat calc(var(--t) * 1.6) var(--ease-out) infinite;
        }

        @keyframes arrive-still-1 {
          0%,
          8% {
            opacity: 0;
          }
          11.5%,
          94% {
            opacity: 1;
          }
          94.5%,
          100% {
            opacity: 0;
          }
        }

        @keyframes arrive-still-2 {
          0%,
          9% {
            opacity: 0;
          }
          12.5%,
          94% {
            opacity: 1;
          }
          94.5%,
          100% {
            opacity: 0;
          }
        }

        @keyframes leave-row {
          0%,
          20% {
            opacity: 1;
          }
          23%,
          94% {
            opacity: 0;
          }
          94.5%,
          100% {
            opacity: 1;
          }
        }

        @keyframes take-seat {
          0%,
          22% {
            opacity: 0;
          }
          25%,
          94% {
            opacity: 1;
          }
          94.5%,
          100% {
            opacity: 0;
          }
        }
      }
    </style>
  </head>
  <body>
    <div
      class="card"
      role="img"
      aria-label="Two edit requests for an unlocked document arrive 18 milliseconds apart. Jun's lands first: that avatar rises out of the request log into the lock's seat, the lock shuts and a lease is issued. Your request steps back and reads queued"
    >
      <div class="doc">
        <svg viewBox="0 0 14 14" aria-hidden="true">
          <path d="M3 1.5h5.2L11 4.3v8.2H3z M8 1.5v3h3" />
        </svg>
        <span>pricing-q4.md</span>
      </div>
      <div class="stage">
        <div class="hold">
          <svg class="lock" viewBox="0 0 16 16" aria-hidden="true">
            <path class="open" d="M11 7.5V4a3 3 0 0 0-6 0v.4" />
            <path class="shut" d="M5 7.5V5.2a3 3 0 0 1 6 0v2.3" />
            <rect x="3" y="7" width="10" height="7.5" rx="1.8" />
          </svg>
          <span class="slot">
            <span class="empty"></span>
            <span class="avatar jun seated">JU</span>
          </span>
          <span class="stack">
            <span class="avail">Available</span>
            <span class="taken">Jun is editing</span>
          </span>
        </div>
        <div class="lease"><i class="fill"></i></div>
        <ol class="log">
          <li>
            <span class="n">1</span>
            <span class="slot">
              <span class="ghost"></span>
              <span class="avatar jun runner">JU</span>
            </span>
            <span class="name">Jun</span>
            <span class="at">:07.412</span>
            <span class="verdict">
              <span class="asked">requested</span>
              <span class="won">granted</span>
            </span>
          </li>
          <li>
            <span class="n">2</span>
            <span class="loser">
              <span class="slot"><span class="avatar you">AK</span></span>
              <span class="name">You</span>
              <span class="at">:07.430</span>
              <span class="verdict">
                <span class="asked">requested</span>
                <span class="wait">queued</span>
              </span>
            </span>
          </li>
        </ol>
      </div>
    </div>
  </body>
</html>

The variant family

Same idea, one axis moved. Compare them side by side before you commit.