Back to the library

Access gate, granted

Variant: Granted

The payoff beat: the bolt retracts into the left leaf, the leaves part, and the verdict draws itself into the gap they just made. Every state change lands in 252-288ms off one shared clock, and the plate dims to zero at 86% so the whole mechanism re-arms inside that dark window and returns closed with no visible rewind.

successaccessdiagramsvg

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>Gate — granted</title>
    <style>
      :root {
        --ivory: #faf9f5;
        --slate: #141413;
        --clay: #d97757;
        --oat: #e3dacc;
        --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
        --loop: 3.6s;
      }

      * {
        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);
      }

      .gate {
        width: 200px;
        height: 180px;
        overflow: visible;
      }

      .gate * {
        transform-box: fill-box;
      }

      .ring,
      .tick,
      .leaf,
      .ring-live,
      .halo,
      .check {
        fill: none;
        stroke: var(--oat);
        stroke-width: 2.5;
      }

      .leaf {
        fill: var(--ivory);
        stroke-linejoin: round;
      }

      .tick {
        stroke-linecap: round;
        opacity: 0.45;
      }

      .bolt {
        fill: none;
        stroke: var(--clay);
        stroke-width: 8;
        stroke-linecap: round;
      }

      .ring-live,
      .halo,
      .check {
        stroke: var(--clay);
      }

      .check {
        stroke-linecap: round;
        stroke-linejoin: round;
      }

      /* One shared clock. Every part reads the same percentages, so the
         beats land in a fixed order: bolt, then doors, then verdict. */
      .plate,
      .bolt,
      .leaf,
      .ring-live,
      .halo,
      .check {
        animation-duration: var(--loop);
        animation-iteration-count: infinite;
        animation-timing-function: linear;
      }

      /* The loop closes on a fade, not a rewind: re-locking the gate would
         undo the promise the animation just made. It dims, resets unseen,
         and comes back closed. */
      .plate {
        transform-origin: center;
        animation-name: reset-breath;
      }

      @keyframes reset-breath {
        0%,
        80% {
          opacity: 1;
          transform: scale(1);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        86%,
        92% {
          opacity: 0;
          transform: scale(0.97);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        100% {
          opacity: 1;
          transform: scale(1);
        }
      }

      /* The bolt withdraws into the left leaf and rides out with it,
         leaving its head visible as a clay stud. */
      .bolt {
        animation-name: withdraw;
      }

      @keyframes withdraw {
        0%,
        5% {
          stroke-dashoffset: 0;
          transform: translateX(0);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        12% {
          stroke-dashoffset: 88;
          transform: translateX(0);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        19%,
        86% {
          stroke-dashoffset: 88;
          transform: translateX(-14px);
        }
        86.5%,
        100% {
          stroke-dashoffset: 0;
          transform: translateX(0);
        }
      }

      .leaf-l {
        animation-name: part-left;
      }

      .leaf-r {
        animation-name: part-right;
      }

      @keyframes part-left {
        0%,
        11% {
          transform: translateX(0);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        19%,
        86% {
          transform: translateX(-14px);
        }
        86.5%,
        100% {
          transform: translateX(0);
        }
      }

      @keyframes part-right {
        0%,
        11% {
          transform: translateX(0);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        19%,
        86% {
          transform: translateX(14px);
        }
        86.5%,
        100% {
          transform: translateX(0);
        }
      }

      /* The frame goes live: oat structure, clay verdict. */
      .ring-live {
        animation-name: go-live;
        opacity: 0;
      }

      @keyframes go-live {
        0%,
        11% {
          opacity: 0;
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        19%,
        86% {
          opacity: 1;
        }
        86.5%,
        100% {
          opacity: 0;
        }
      }

      .halo {
        transform-origin: center;
        animation-name: pulse;
        opacity: 0;
      }

      @keyframes pulse {
        0%,
        18% {
          transform: scale(1);
          opacity: 0;
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        22% {
          opacity: 0.5;
        }
        44% {
          transform: scale(1.32);
          opacity: 0;
        }
        86.5%,
        100% {
          transform: scale(1);
          opacity: 0;
        }
      }

      /* The verdict draws itself in the gap the doors opened. */
      .check {
        animation-name: draw;
        stroke-dashoffset: 100;
        opacity: 0;
      }

      @keyframes draw {
        0%,
        15.9% {
          stroke-dashoffset: 100;
          opacity: 0;
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        16% {
          stroke-dashoffset: 100;
          opacity: 1;
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        23.5%,
        86% {
          stroke-dashoffset: 0;
          opacity: 1;
        }
        86.5%,
        100% {
          stroke-dashoffset: 100;
          opacity: 0;
        }
      }

      @media (hover: hover) and (pointer: fine) {
        .gate {
          transition: transform 200ms var(--ease-out);
        }

        .gate:hover {
          transform: scale(1.015);
        }
      }

      /* Reduced motion keeps the whole story — bolt, doors, verdict — and
         tells it in opacity alone. Gentler, not gone. */
      @media (prefers-reduced-motion: reduce) {
        .plate {
          animation-name: reset-fade;
        }

        .bolt {
          animation-name: withdraw-fade;
        }

        .leaf-l,
        .leaf-r {
          animation-name: leaf-fade;
        }

        .halo {
          animation: none;
          opacity: 0;
        }

        @keyframes reset-fade {
          0%,
          80% {
            opacity: 1;
          }
          86%,
          92% {
            opacity: 0;
          }
          100% {
            opacity: 1;
          }
        }

        @keyframes withdraw-fade {
          0%,
          5% {
            opacity: 1;
          }
          14%,
          86% {
            opacity: 0;
          }
          86.5%,
          100% {
            opacity: 1;
          }
        }

        @keyframes leaf-fade {
          0%,
          11% {
            opacity: 1;
          }
          19%,
          86% {
            opacity: 0.18;
          }
          86.5%,
          100% {
            opacity: 1;
          }
        }

        @media (hover: hover) and (pointer: fine) {
          .gate:hover {
            transform: none;
          }
        }
      }
    </style>
  </head>
  <body>
    <svg
      class="gate"
      viewBox="0 0 200 180"
      role="img"
      aria-label="An access gate opening after access is granted"
    >
      <g class="plate">
        <circle class="halo" cx="100" cy="90" r="48" />
        <circle class="ring" cx="100" cy="90" r="48" />
        <circle class="ring-live" cx="100" cy="90" r="48" />

        <line class="tick" x1="138.2" y1="51.8" x2="141.7" y2="48.3" />
        <line class="tick" x1="61.8" y1="51.8" x2="58.3" y2="48.3" />
        <line class="tick" x1="61.8" y1="128.2" x2="58.3" y2="131.7" />
        <line class="tick" x1="138.2" y1="128.2" x2="141.7" y2="131.7" />

        <path class="leaf leaf-l" d="M100 58 A32 32 0 0 0 100 122 Z" />
        <path class="leaf leaf-r" d="M100 58 A32 32 0 0 1 100 122 Z" />

        <line
          class="bolt"
          x1="76"
          y1="90"
          x2="124"
          y2="90"
          pathLength="100"
          stroke-dasharray="100 100"
        />

        <path class="check" d="M88 91 L96 100 L112 79" pathLength="100" stroke-dasharray="100" />
      </g>
    </svg>
  </body>
</html>

The variant family

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