# Silver Cloche Pricing in WebGL Where the Chosen Dome Lifts Off Its Plate on a White Tablecloth in Window Light

> Free React pricing section in raw WebGL: three polished silver cloches on a linen tablecloth; choosing a plan lifts its dome to show the price on the plate.

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/silver-cloche-pricing-in-webgl-where-the-chosen-dome-lifts-off-its-plate-on-a-white-tablecloth-in-window-light).

- Page, with a live preview: https://shadcn.exchange/r/weinc/silver-cloche-pricing-in-webgl-where-the-chosen-dome-lifts-off-its-plate-on-a-white-tablecloth-in-window-light
- Screenshot: https://shadcn.exchange/shot/weinc/silver-cloche-pricing-in-webgl-where-the-chosen-dome-lifts-off-its-plate-on-a-white-tablecloth-in-window-light/light.png
- Registry item: https://we.inc/r/silver-cloche-pricing-in-webgl-where-the-chosen-dome-lifts-off-its-plate-on-a-white-tablecloth-in-window-light.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/silver-cloche-pricing-in-webgl-where-the-chosen-dome-lifts-off-its-plate-on-a-white-tablecloth-in-window-light.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `framer-motion`

## Files

### `components/we-inc/silver-cloche-pricing-in-webgl-where-the-chosen-dome-lifts-off-its-plate-on-a-white-tablecloth-in-window-light.tsx`

```tsx
import { useEffect, useRef, useState } from "react";
import { motion, useReducedMotion } from "framer-motion";

/** The product and its three plans. These are example prices: change them to your own. */
const product = { name: "Brasswick", line: "Reservations and floor plans for restaurants" };
const plans = [
  {
    name: "Bistro",
    price: 19,
    pitch: "One room, every table accounted for.",
    features: ["1 dining room, 40 covers", "Online bookings and walk-ins", "Reminder texts to guests"],
    dome: 0.44, // cloche radius in metres; the plate is sized from it
  },
  {
    name: "Brasserie",
    price: 49,
    pitch: "Two sittings a night without a clash.",
    features: ["Up to 3 rooms, 160 covers", "Drag-and-drop floor plan", "Deposits for large parties", "Waitlist on a tablet"],
    dome: 0.5,
  },
  {
    name: "Grand Salon",
    price: 119,
    pitch: "Private dining, terraces and events.",
    features: ["Unlimited rooms and covers", "Event menus and run sheets", "Group booking pages", "A named setup host"],
    dome: 0.56,
  },
];

const GAP = 0.3; // tablecloth between neighbouring plates
const LIFT = 0.5; // how high the chosen cloche is raised, in metres
const TILT = 1.12; // how far its front edge tips up, in radians
const DEMO = true; // lifts the cloches in turn until somebody touches it
const SERIF = '"Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif';
const GROTESK = '"Inter Tight", "Inter", ui-sans-serif, system-ui, sans-serif';
const INK = "#1f1b17";
const CLOTH = "#ebe8e2";

const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));
const PLATE = (a: number) => a * 1.24;
const XS = (() => {
  const total = plans.reduce((s, p) => s + PLATE(p.dome) * 2, 0) + GAP * (plans.length - 1);
  let x = -total / 2;
  return plans.map((p) => {
    const c = x + PLATE(p.dome);
    x += PLATE(p.dome) * 2 + GAP;
    return c;
  });
})();
const domeH = (a: number) => a * 0.86 + 0.03;
const SEAT = 0.038; // where the cloche rim rests on the plate
const SUN_DIR = (() => {
  const v = [-0.66, 0.56, -0.3];
  const l = Math.hypot(v[0], v[1], v[2]);
  return v.map((x) => x / l);
})();

const GLSL = `#version 300 es
precision highp float;
const vec3 L = vec3(${SUN_DIR.map((v) => v.toFixed(4)).join(", ")});
const vec3 SUN = vec3(1.0, 0.93, 0.82) * 3.2;
const vec3 SKY = vec3(0.82, 0.85, 0.9) * 0.7;
const float WZ = -3.6;
uniform vec4 uC[3];      // x, z, dome radius, dome height
uniform vec3 uO[3];      // where the cloche rim centre is now
uniform vec2 uCS[3];     // cos and sin of its tilt
uniform float uR[3];     // plate radius
uniform sampler2D uTex;
uniform vec3 uCam, uRt, uUp, uFw;
uniform float uTan, uAspect, uFocus, uAperture, uTime;
uniform vec2 uShift;
in vec2 vUv;
out vec4 o;

float ign(vec2 p) { return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))); }
float hash(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
float vnoise(vec2 p) {
  vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);
  return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);
}
float fbm(vec2 p) { return 0.5 * vnoise(p) + 0.25 * vnoise(p * 2.1 + 7.3) + 0.125 * vnoise(p * 4.3 + 1.7) + 0.0625 * vnoise(p * 8.9); }
float smin(float a, float b, float k) { float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k * h * (1.0 - h); }

vec3 toL(vec3 d, int i) { vec2 r = uCS[i]; return vec3(d.x, d.y * r.x - d.z * r.y, d.y * r.y + d.z * r.x); }

float ell(vec3 q, vec3 r) { float k0 = length(q / r), k1 = length(q / (r * r)); return k0 * (k0 - 1.0) / max(k1, 1e-4); }

// the cloche in its own frame: a thin spun dome, a rolled bead at the rim and a turned knob
float cloche(vec3 q, float a, float b) {
  float rho = length(q.xz);
  float shell = abs(ell(q, vec3(a, b, a))) - 0.009;
  shell = max(shell, -q.y);
  float bead = length(vec2(rho - a, q.y - 0.016)) - 0.017;
  float band = length(vec2(rho - a * 0.985, q.y - 0.075)) - 0.006;
  float d = min(min(shell, bead), band);
  float stem = max(rho - 0.02 - 0.012 * smoothstep(b + 0.035, b, q.y), abs(q.y - b - 0.02) - 0.03);
  float knob = ell(q - vec3(0.0, b + 0.072, 0.0), vec3(0.052, 0.036, 0.052));
  return min(d, smin(stem, knob, 0.02));
}
float plateTop(float rho, float R) {
  return 0.02 + 0.026 * smoothstep(0.62 * R, 0.86 * R, rho) - 0.02 * smoothstep(0.93 * R, R, rho);
}
float plate(vec3 p, int i) {
  float R = uR[i];
  float rho = length(p.xz - uC[i].xy);
  return max(rho - R, max(p.y - plateTop(rho, R), 0.004 - p.y)) * 0.8;
}

// id: 0..2 cloches, 3..5 plates
vec2 map(vec3 p) {
  vec2 res = vec2(1e9, -1.0);
  for (int i = 0; i < 3; i++) {
    vec4 C = uC[i];
    float pb = length(vec2(max(length(p.xz - C.xy) - uR[i], 0.0), max(p.y - 0.05, 0.0)));
    float dp = pb > 0.1 ? pb : plate(p, i);
    if (dp < res.x) res = vec2(dp, float(3 + i));
    vec3 q = toL(p - uO[i], i);
    float bound = length(q - vec3(0.0, C.w * 0.5, 0.0)) - max(C.z, C.w) - 0.12;
    float dc = bound > 0.1 ? bound : cloche(q, C.z, C.w);
    if (dc < res.x) res = vec2(dc, float(i));
  }
  return res;
}
vec3 nrm(vec3 p) {
  vec2 e = vec2(0.0006, -0.0006);
  return normalize(e.xyy * map(p + e.xyy).x + e.yyx * map(p + e.yyx).x + e.yxy * map(p + e.yxy).x + e.xxx * map(p + e.xxx).x);
}
float shadow(vec3 p) {
  float s = 1.0, t = 0.015;
  for (int k = 0; k < 32; k++) {
    float h = map(p + L * t).x;
    s = min(s, 7.0 * h / t);
    t += clamp(h, 0.012, 0.2);
    if (s < 0.01 || t > 3.0) break;
  }
  return clamp(s, 0.0, 1.0);
}
float ao(vec3 p, vec3 n) {
  float a = 0.0, w = 1.0;
  for (int k = 1; k <= 5; k++) {
    float h = 0.014 * float(k * k);
    a += w * (h - map(p + n * h).x);
    w *= 0.6;
  }
  return clamp(1.0 - 2.4 * a, 0.0, 1.0);
}

// sun through a tall sash window on the left: glazing bars and a curtain edge break it up
float sash(vec3 p) {
  vec3 a = normalize(cross(L, vec3(0.0, 1.0, 0.0))), b = cross(a, L);
  vec2 w = vec2(dot(p, a), dot(p, b)) - vec2(0.3, -0.4);
  float pen = 0.045 + 0.06 * clamp(dot(p, -L) + 2.0, 0.0, 4.0);
  vec2 e = vec2(2.6, 2.2) - abs(w);
  float m = smoothstep(-pen, pen, e.x) * smoothstep(-pen, pen, e.y);
  vec2 g = abs(fract(w / vec2(2.6, 1.5) + 0.5) - 0.5) * vec2(2.6, 1.5);
  m *= smoothstep(0.03, 0.03 + pen, g.x) * smoothstep(0.02, 0.02 + pen, g.y);
  float curtain = smoothstep(0.0, 0.35, w.x + 2.4 + 0.12 * sin(w.y * 3.0 + uTime * 0.4));
  return m * (0.25 + 0.75 * curtain);
}

// the dining room the silver sees: dark panelling, a cream ceiling, the sash window and the bright cloth
vec3 env(vec3 r) {
  float up = r.y;
  vec3 c = mix(vec3(0.1, 0.08, 0.065), vec3(0.62, 0.6, 0.56), smoothstep(0.08, 0.5, up));
  // a dresser and pictures along the wall behind the camera
  float az = atan(r.x, r.z);
  c *= 0.8 + 0.3 * smoothstep(0.42, 0.5, abs(fract(az * 4.0) - 0.5)) * step(up, 0.18);
  vec2 fr = abs(vec2(fract(az * 0.7 + 0.3) - 0.5, up - 0.4));
  c = mix(c, vec3(0.03, 0.025, 0.02), step(fr.x, 0.16) * step(fr.y, 0.13) * (1.0 - step(fr.x, 0.13) * step(fr.y, 0.1)));
  c += vec3(1.0, 0.72, 0.42) * 2.2 * smoothstep(0.985, 0.998, dot(r, normalize(vec3(0.6, 0.28, 0.75))));
  // the tablecloth below the horizon
  c = mix(c, vec3(0.86, 0.84, 0.8) * 1.15, smoothstep(0.03, -0.14, up));
  // the window: two tall sashes with bars, behind and to the left
  vec3 wd = normalize(vec3(-0.85, 0.3, -0.42));
  float d = dot(r, wd);
  if (d > 0.0) {
    vec3 a = normalize(cross(wd, vec3(0.0, 1.0, 0.0))), b = cross(a, wd);
    vec2 q = vec2(dot(r, a), dot(r, b)) / d;
    float pane = smoothstep(0.03, 0.0, max(abs(q.x) - 0.5, abs(q.y - 0.1) - 0.75));
    vec2 g = abs(fract(q / vec2(0.5, 0.5) + 0.5) - 0.5) * 0.5;
    pane *= smoothstep(0.008, 0.02, g.x) * smoothstep(0.008, 0.02, g.y);
    c = mix(c, vec3(1.0, 0.98, 0.94) * 10.0, pane);
  }
  // a softer window behind the camera on the right
  vec3 w2 = normalize(vec3(0.7, 0.32, 0.64));
  float d2 = dot(r, w2);
  if (d2 > 0.0) {
    vec3 a = normalize(cross(w2, vec3(0.0, 1.0, 0.0))), b = cross(a, w2);
    vec2 q = vec2(dot(r, a), dot(r, b)) / d2;
    float pane = smoothstep(0.05, 0.0, max(abs(q.x) - 0.22, abs(q.y) - 0.42));
    pane *= smoothstep(0.0, 0.015, abs(q.x)) * smoothstep(0.0, 0.015, abs(q.y - 0.05));
    c = mix(c, vec3(0.88, 0.93, 1.0) * 3.2, pane);
  }
  return c;
}

vec3 clothAlb(vec3 p) {
  float weave = vnoise(p.xz * vec2(70.0, 9.0)) * 0.5 + vnoise(p.xz * vec2(8.0, 64.0)) * 0.5;
  return vec3(0.86, 0.85, 0.82) * (0.975 + 0.035 * weave);
}
// ironed fold lines run across the cloth in a grid
vec3 clothN(vec3 p) {
  float cx = (fract(p.x / 1.45 + 0.31) - 0.5) * 1.45;
  float cz = (fract(p.z / 1.2 + 0.18) - 0.5) * 1.2;
  float bx = exp(-cx * cx / 0.0016), bz = exp(-cz * cz / 0.0012);
  float gx = -0.1 * bx * (2.0 * cx / 0.0016) * 0.04, gz = 0.08 * bz * (2.0 * cz / 0.0012) * 0.04;
  gx += (vnoise(p.xz * 3.0) - 0.5) * 0.04;
  return normalize(vec3(-gx, 1.0, -gz));
}
vec3 shadeCloth(vec3 p, bool full) {
  vec3 n = clothN(p);
  float sh = (full ? shadow(p + vec3(0.0, 0.004, 0.0)) : 1.0) * sash(p);
  float oc = full ? ao(p, vec3(0.0, 1.0, 0.0)) : 1.0;
  vec3 alb = clothAlb(p);
  vec3 col = alb * (SUN * max(dot(n, L), 0.0) * sh + SKY * oc * (0.8 + 0.2 * n.y));
  // light that the polished domes throw back onto the cloth
  for (int i = 0; i < 3; i++) {
    vec3 d = uO[i] + vec3(0.0, uC[i].w * 0.5, 0.0) - p;
    col += alb * SUN * 0.035 * uC[i].z / (dot(d, d) + 0.15) * oc;
  }
  return col;
}

vec3 platePaint(vec3 p, int i, out float ink) {
  vec2 d = p.xz - uC[i].xy;
  float Rt = uR[i] * 0.64;
  float u = d.x / (2.0 * Rt) + 0.5, v = 0.5 - d.y / (2.0 * Rt);
  ink = 0.0;
  if (u > 0.0 && u < 1.0 && v > 0.0 && v < 1.0) ink = texture(uTex, vec2(u, (float(i) + v) / 3.0)).r;
  return vec3(0.035, 0.016, 0.008);
}

// what a reflected ray off the silver sees: the other pieces, the cloth, or the room
vec3 bounce(vec3 ro, vec3 rd) {
  float t = 0.02;
  for (int k = 0; k < 36; k++) {
    vec2 h = map(ro + rd * t);
    if (h.x < 0.002) {
      vec3 p = ro + rd * t;
      if (h.y > 2.5) {
        int i = int(h.y + 0.5) - 3;
        float ink;
        vec3 ic = platePaint(p, i, ink);
        vec3 alb = mix(vec3(0.9, 0.89, 0.86), ic, ink);
        return alb * (SUN * 0.5 * sash(p) + SKY);
      }
      vec3 n = nrm(p);
      return env(reflect(rd, n)) * vec3(0.92, 0.9, 0.86) * 0.85;
    }
    t += h.x;
    if (t > 4.0) break;
  }
  if (rd.y < 0.0) {
    float tp = -ro.y / rd.y;
    vec3 p = ro + rd * tp;
    return shadeCloth(p, false) * 0.92;
  }
  return env(rd);
}

vec3 shadeSilver(int i, vec3 p, vec3 rd, vec3 n) {
  vec3 q = toL(p - uO[i], i);
  vec2 s = vec2(atan(q.x, q.z) * uC[i].z, q.y);
  // fine spinning lines from the lathe and a few soft polishing smears
  float lines = vnoise(vec2(s.y * 420.0, s.x * 2.0)) - 0.5;
  float smear = fbm(s * 5.0 + float(i) * 4.0);
  vec3 nb = normalize(n + (vec3(0.0, 1.0, 0.0) * lines * 0.006) + (vec3(smear - 0.5) * 0.006));
  vec3 R = reflect(rd, nb);
  vec3 refl = bounce(p + nb * 0.006, R);
  // soften the room a little where the polish is smeared
  refl = mix(refl, env(normalize(R + (vec3(vnoise(s * 9.0), vnoise(s * 9.0 + 3.0), 0.0) - 0.5) * 0.2)), 0.25 * smoothstep(0.55, 0.8, smear));
  float cosT = clamp(dot(-rd, nb), 0.0, 1.0);
  vec3 F0 = vec3(0.95, 0.93, 0.88);
  vec3 F = F0 + (1.0 - F0) * pow(1.0 - cosT, 5.0);
  float oc = ao(p, n);
  float sh = shadow(p + n * 0.01);
  vec3 col = refl * F * mix(0.55, 1.0, oc);
  col += SUN * sh * sash(p) * (30.0 * pow(max(dot(R, L), 0.0), 900.0) + 0.5 * pow(max(dot(R, L), 0.0), 60.0));
  return col;
}

vec3 shadePlate(int i, vec3 p, vec3 rd, vec3 n) {
  float ink;
  vec3 ic = platePaint(p, i, ink);
  // only paint the flat well of the plate
  ink *= step(0.5, n.y) * smoothstep(0.7 * uR[i], 0.64 * uR[i], length(p.xz - uC[i].xy));
  float rr = length(p.xz - uC[i].xy) / uR[i];
  float band = smoothstep(0.004, 0.0, abs(rr - 0.9) - 0.012);
  float gild = smoothstep(0.003, 0.0, abs(rr - 0.875) - 0.003);
  vec3 alb = mix(vec3(0.93, 0.925, 0.9), ic, ink);
  alb = mix(alb, vec3(0.06, 0.2, 0.15), band);
  alb = mix(alb, vec3(0.6, 0.42, 0.16), gild);
  float sh = shadow(p + n * 0.006) * sash(p);
  float oc = ao(p, n);
  vec3 col = alb * (SUN * max(dot(n, L), 0.0) * sh + SKY * oc);
  // porcelain glaze
  float F = 0.04 + 0.96 * pow(1.0 - clamp(dot(-rd, n), 0.0, 1.0), 5.0);
  vec3 R = reflect(rd, n);
  vec3 refl = env(R) * mix(0.4, 1.0, oc) + SUN * sh * 26.0 * pow(max(dot(R, L), 0.0), 700.0);
  return mix(col, refl, F * (1.0 - 0.4 * ink));
}

vec3 shadeWall(vec3 p) {
  vec3 alb = mix(vec3(0.74, 0.7, 0.64), vec3(0.8, 0.76, 0.7), fbm(p.xy * 1.2));
  float lit = sash(p + vec3(0.0, 0.0, 0.0));
  return alb * (SUN * 0.45 * lit + SKY * 0.85);
}

vec3 tonemap(vec3 x) { x *= 0.6; return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0); }

void main() {
  vec2 ndc = vUv * 2.0 - 1.0 - uShift;
  vec3 rd = normalize(uFw + uRt * (ndc.x * uTan * uAspect) + uUp * (ndc.y * uTan));
  float tp = 1e9; int pid = -1;
  if (rd.y < 0.0) { float t = -uCam.y / rd.y; vec3 h = uCam + rd * t; if (h.z > WZ) { tp = t; pid = 0; } }
  if (pid < 0 && rd.z < 0.0) { tp = (WZ - uCam.z) / rd.z; pid = 1; }
  float t = max(0.0, (uCam.y - 1.6) / max(-rd.y, 0.05));
  float id = -1.0;
  float tmax = min(tp, 40.0);
  for (int k = 0; k < 120; k++) {
    vec2 h = map(uCam + rd * t);
    if (h.x < 0.0005 * t) { id = h.y; break; }
    t += h.x * 0.85;
    if (t > tmax) break;
  }
  vec3 col;
  float depth;
  if (id >= 0.0 && t < tmax) {
    vec3 p = uCam + rd * t;
    vec3 n = nrm(p);
    int k = int(id + 0.5);
    col = k < 3 ? shadeSilver(k, p, rd, n) : shadePlate(k - 3, p, rd, n);
    depth = t;
  } else {
    vec3 p = uCam + rd * tp;
    col = pid == 0 ? shadeCloth(p, true) : shadeWall(p);
    depth = tp;
  }
  // haze in the window light
  col += vec3(1.0, 0.93, 0.84) * 0.03 * smoothstep(0.5, -1.0, dot(rd, vec3(1.0, 0.0, 0.0)));
  float z = depth * dot(rd, uFw);
  float coc = clamp(uAperture * (1.0 - uFocus / max(z, 0.05)), -1.0, 1.0);
  o = vec4(pow(tonemap(col), vec3(1.0 / 2.2)), 0.5 + 0.5 * coc);
}`;

const VS = `#version 300 es
out vec2 vUv;
void main() { vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2)); vUv = p; gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); }`;

const FS_LENS = `#version 300 es
precision highp float;
uniform sampler2D uScene;
uniform vec2 uPx;
uniform float uMaxR, uTime;
in vec2 vUv;
out vec4 o;
float ign(vec2 p) { return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))); }
void main() {
  vec4 c0 = texture(uScene, vUv);
  float s0 = c0.a * 2.0 - 1.0, coc0 = abs(s0);
  vec3 acc = c0.rgb; float wsum = 1.0;
  float rot = 6.2832 * ign(gl_FragCoord.xy);
  for (int k = 1; k < 20; k++) {
    float r = sqrt(float(k) / 20.0) * uMaxR;
    float a = float(k) * 2.39996 + rot;
    vec4 s = texture(uScene, vUv + vec2(cos(a), sin(a)) * r * uPx);
    float sc = s.a * 2.0 - 1.0, cs = abs(sc);
    if (sc > s0) cs = min(cs, coc0 * 1.6 + 0.02);
    float w = smoothstep(r - 1.0, r + 1.0, cs * uMaxR);
    acc += s.rgb * w; wsum += w;
  }
  vec3 col = acc / wsum;
  const vec3 LU = vec3(0.299, 0.587, 0.114);
  vec3 cNW = texture(uScene, vUv + vec2(-1.0, -1.0) * uPx).rgb, cNE = texture(uScene, vUv + vec2(1.0, -1.0) * uPx).rgb;
  vec3 cSW = texture(uScene, vUv + vec2(-1.0, 1.0) * uPx).rgb, cSE = texture(uScene, vUv + vec2(1.0, 1.0) * uPx).rgb;
  float lNW = dot(cNW, LU), lNE = dot(cNE, LU), lSW = dot(cSW, LU), lSE = dot(cSE, LU), lM = dot(c0.rgb, LU);
  float lMin = min(lM, min(min(lNW, lNE), min(lSW, lSE))), lMax = max(lM, max(max(lNW, lNE), max(lSW, lSE)));
  vec2 dir = vec2(-((lNW + lNE) - (lSW + lSE)), (lNW + lSW) - (lNE + lSE));
  float rcp = 1.0 / (min(abs(dir.x), abs(dir.y)) + max((lNW + lNE + lSW + lSE) * 0.03125, 0.0078));
  dir = clamp(dir * rcp, -8.0, 8.0) * uPx;
  vec3 fa = 0.5 * (texture(uScene, vUv + dir * (1.0 / 3.0 - 0.5)).rgb + texture(uScene, vUv + dir * (2.0 / 3.0 - 0.5)).rgb);
  vec3 fb = fa * 0.5 + 0.25 * (texture(uScene, vUv - dir * 0.5).rgb + texture(uScene, vUv + dir * 0.5).rgb);
  float lB = dot(fb, LU);
  vec3 fx = (lB < lMin || lB > lMax) ? fa : fb;
  col = mix(fx, col, smoothstep(0.05, 0.3, coc0));
  vec2 q = vUv - 0.5;
  col *= 1.0 - 0.28 * dot(q, q);
  col += (ign(gl_FragCoord.xy + fract(uTime) * 61.0) - 0.5) * 0.016;
  o = vec4(col, 1.0);
}`;

function program(gl: WebGL2RenderingContext, vs: string, fs: string) {
  const p = gl.createProgram()!;
  for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]] as const) {
    const s = gl.createShader(type)!;
    gl.shaderSource(s, src);
    gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s) || "shader");
    gl.attachShader(p, s);
  }
  gl.linkProgram(p);
  if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p) || "link");
  return p;
}

/** What is piped onto each plate in chocolate: plan name, the example price and its label. One square per plan. */
function artwork() {
  const S = 1024;
  const cv = document.createElement("canvas");
  cv.width = S;
  cv.height = S * plans.length;
  const ctx = cv.getContext("2d")!;
  ctx.fillStyle = "#000";
  ctx.fillRect(0, 0, cv.width, cv.height);
  ctx.fillStyle = "#fff";
  const tracked = (s: string, y: number, font: string, gap: number) => {
    ctx.font = font;
    ctx.textAlign = "left";
    const w = [...s].map((c) => ctx.measureText(c).width + gap);
    let x = S / 2 - (w.reduce((a, b) => a + b, 0) - gap) / 2;
    [...s].forEach((c, i) => {
      ctx.fillText(c, x, y);
      x += w[i];
    });
  };
  plans.forEach((plan, i) => {
    // the texture is flipped on upload, so plan 0 sits in the bottom square
    ctx.save();
    ctx.translate(0, (plans.length - 1 - i) * S);
    // the plate is seen at a low angle, so the lettering is drawn tall to read true
    ctx.scale(1, 1.7);
    tracked(plan.name.toUpperCase(), 150, `700 ${plan.name.length > 9 ? 74 : 88}px ${GROTESK}`, 14);
    ctx.fillRect(S / 2 - 80, 172, 160, 6);
    tracked(`$${plan.price}`, 430, `italic 600 ${plan.price > 99 ? 290 : 330}px ${SERIF}`, -10);
    tracked("A MONTH · EXAMPLE", 520, `700 62px ${GROTESK}`, 8);
    ctx.restore();
  });
  return cv;
}

type Sim = { l: number[]; vl: number[]; a: number[]; va: number[]; dip: number[]; plan: number; dirty: boolean; touched: boolean };

/** The same table drawn with plain elements, for browsers without WebGL 2. */
function Still({ plan, marks }: { plan: number; marks: number[] }) {
  return (
    <div aria-hidden="true" className="absolute inset-0 overflow-hidden" style={{ background: `linear-gradient(180deg, #8d8276 0%, #b3a99c 26%, #dcd8d1 27%, ${CLOTH} 60%, #f4f2ee 100%)` }}>
      <div className="absolute left-0 top-0 h-full w-[60%]" style={{ background: "linear-gradient(110deg, rgba(255,250,240,0.6), rgba(255,250,240,0) 70%)" }} />
      {plans.map((p, i) => {
        const up = i === plan;
        const w = p.dome * 30;
        return (
          <div key={p.name} className="absolute -translate-x-1/2" style={{ left: `${marks[i]}%`, bottom: "26%", width: `${w}%` }}>
            <div className="absolute -bottom-[10%] left-[-14%] h-[34%] w-[128%] rounded-[50%] border border-black/10 bg-[#f6f5f2] shadow-[0_8px_18px_rgba(0,0,0,0.18)]">
              {up && (
                <div className="absolute inset-0 grid place-items-center text-center text-[#2a1810]" style={{ fontFamily: SERIF }}>
                  <span className="text-[clamp(14px,2.6vw,30px)] italic leading-none">${p.price}</span>
                </div>
              )}
            </div>
            <div
              className="relative rounded-t-full"
              style={{
                aspectRatio: "2 / 1.6",
                transform: up ? "translateY(-70%) rotate(-12deg)" : "none",
                background: "radial-gradient(ellipse at 34% 30%, #ffffff 0%, #e9e9e6 12%, #8c8b88 42%, #3c3b39 70%, #b9b7b2 92%)",
                boxShadow: "inset 0 -6px 10px rgba(0,0,0,0.35)",
              }}
            >
              <div className="absolute left-1/2 top-[-9%] h-[12%] w-[14%] -translate-x-1/2 rounded-full" style={{ background: "radial-gradient(circle at 35% 30%, #fff, #8a8985 60%, #3a3937)" }} />
            </div>
          </div>
        );
      })}
    </div>
  );
}

export default function SilverClochePricing() {
  const reduce = useReducedMotion();
  const wrapRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [plan, setPlan] = useState(1);
  const [mode, setMode] = useState<"pending" | "gl" | "still">("pending");
  const [marks, setMarks] = useState<number[]>([24, 50, 77]);
  const [handled, setHandled] = useState(false);
  const sim = useRef<Sim>({ l: [0, 1, 0], vl: [0, 0, 0], a: [0, TILT, 0], va: [0, 0, 0], dip: [0, 0, 0], plan: 1, dirty: true, touched: false });

  useEffect(() => {
    const s = sim.current;
    if (s.plan !== plan) {
      s.plan = plan;
      s.dip[plan] = 0.11; // a waiter's small press down before the lift
    }
    s.dirty = true;
  }, [plan]);

  useEffect(() => {
    const wrap = wrapRef.current;
    const canvas = canvasRef.current;
    if (!wrap || !canvas) return;
    const gl = canvas.getContext("webgl2", { antialias: false, alpha: false, powerPreference: "high-performance" });
    if (!gl) {
      setMode("still");
      return;
    }
    const s = sim.current;
    let pScene: WebGLProgram, pLens: WebGLProgram;
    try {
      pScene = program(gl, VS, GLSL);
      pLens = program(gl, VS, FS_LENS);
    } catch {
      setMode("still");
      return;
    }
    setMode("gl");

    const art = gl.createTexture();
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, art);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, artwork());
    gl.generateMipmap(gl.TEXTURE_2D);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);

    const vao = gl.createVertexArray();
    const fbo = gl.createFramebuffer();
    const tex = gl.createTexture();
    let W = 0, H = 0, scale = Math.min(window.devicePixelRatio || 1, 2), cssW = 1, cssH = 1;
    const TAN = Math.tan((10 * Math.PI) / 180);
    const cam = { pos: [0, 2, 10], rt: [1, 0, 0], up: [0, 1, 0], fw: [0, 0, -1], shift: [0, 0], focus: 8 };

    const aim = (sx: number, sy: number) => {
      const aspect = cssW / cssH;
      const wide = aspect > 1.2;
      const dist = Math.max(9.4, (wide ? 2.4 : 2.2) / (TAN * aspect));
      const el = 0.52;
      const target = [0, 0.32, 0.05];
      cam.pos = [target[0] + sx, target[1] + Math.sin(el) * dist + sy, target[2] + Math.cos(el) * dist];
      let fw = [target[0] - cam.pos[0], target[1] - cam.pos[1], target[2] - cam.pos[2]];
      const fl = Math.hypot(fw[0], fw[1], fw[2]);
      fw = fw.map((v) => v / fl);
      let rt = [-fw[2], 0, fw[0]];
      const rl = Math.hypot(rt[0], rt[2]);
      rt = rt.map((v) => v / rl);
      cam.fw = fw;
      cam.rt = rt;
      cam.up = [rt[1] * fw[2] - rt[2] * fw[1], rt[2] * fw[0] - rt[0] * fw[2], rt[0] * fw[1] - rt[1] * fw[0]];
      cam.shift = wide ? [0.12, -0.36] : [0, -0.38];
      cam.focus = fl;
      return aspect;
    };
    const project = (p: number[]) => {
      const d = [p[0] - cam.pos[0], p[1] - cam.pos[1], p[2] - cam.pos[2]];
      const z = d[0] * cam.fw[0] + d[1] * cam.fw[1] + d[2] * cam.fw[2];
      const x = (d[0] * cam.rt[0] + d[1] * cam.rt[1] + d[2] * cam.rt[2]) / (z * TAN * (cssW / cssH)) + cam.shift[0];
      const y = (d[0] * cam.up[0] + d[1] * cam.up[1] + d[2] * cam.up[2]) / (z * TAN) + cam.shift[1];
      return [(x * 0.5 + 0.5) * cssW, (0.5 - y * 0.5) * cssH];
    };

    const resize = () => {
      cssW = wrap.clientWidth;
      cssH = wrap.clientHeight;
      let k = scale;
      if (cssW * cssH * k * k > 1.5e6) k = Math.sqrt(1.5e6 / (cssW * cssH));
      const w = Math.max(2, Math.round(cssW * k)), h = Math.max(2, Math.round(cssH * k));
      aim(0, 0);
      setMarks(XS.map((x) => clamp((project([x, 0, 0])[0] / cssW) * 100, cssW < 640 ? 19 : 8, cssW < 640 ? 81 : 92)));
      if (w === W && h === H) return;
      W = w;
      H = h;
      canvas.width = W;
      canvas.height = H;
      gl.activeTexture(gl.TEXTURE1);
      gl.bindTexture(gl.TEXTURE_2D, tex);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, W, H, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
      gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
      gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
      s.dirty = true;
    };
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(wrap);

    const U = (p: WebGLProgram, n: string) => gl.getUniformLocation(p, n);
    let pointer = [0, 0], view = [0, 0];

    const draw = (t: number) => {
      const aspect = aim(view[0], view[1]);
      gl.bindVertexArray(vao);
      gl.viewport(0, 0, W, H);
      gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
      gl.useProgram(pScene);
      const C: number[] = [], O: number[] = [], CS: number[] = [], R: number[] = [];
      plans.forEach((p, i) => {
        const l = s.l[i];
        C.push(XS[i], 0, p.dome, domeH(p.dome));
        // lifted up and drawn back toward the waiter, front edge rising so the diner sees the plate
        O.push(XS[i], SEAT + l * (LIFT + (reduce ? 0 : 0.008 * Math.sin(t * 1.6 + i))), -l * p.dome * 1.05);
        CS.push(Math.cos(s.a[i]), Math.sin(s.a[i]));
        R.push(PLATE(p.dome));
      });
      gl.uniform4fv(U(pScene, "uC"), C);
      gl.uniform3fv(U(pScene, "uO"), O);
      gl.uniform2fv(U(pScene, "uCS"), CS);
      gl.uniform1fv(U(pScene, "uR"), R);
      gl.uniform1i(U(pScene, "uTex"), 0);
      gl.uniform3fv(U(pScene, "uCam"), cam.pos);
      gl.uniform3fv(U(pScene, "uRt"), cam.rt);
      gl.uniform3fv(U(pScene, "uUp"), cam.up);
      gl.uniform3fv(U(pScene, "uFw"), cam.fw);
      gl.uniform1f(U(pScene, "uTan"), TAN);
      gl.uniform1f(U(pScene, "uAspect"), aspect);
      gl.uniform2fv(U(pScene, "uShift"), cam.shift);
      gl.uniform1f(U(pScene, "uFocus"), cam.focus);
      gl.uniform1f(U(pScene, "uAperture"), 1.5);
      gl.uniform1f(U(pScene, "uTime"), t);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
      gl.useProgram(pLens);
      gl.uniform1i(U(pLens, "uScene"), 1);
      gl.uniform2f(U(pLens, "uPx"), 1 / W, 1 / H);
      gl.uniform1f(U(pLens, "uMaxR"), H * 0.007);
      gl.uniform1f(U(pLens, "uTime"), reduce ? 0 : t);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
    };

    // The lift is an underdamped spring so the dome overshoots a touch; the tilt is a second, looser spring that trails it.
    const physics = (dt: number) => {
      plans.forEach((_, i) => {
        let target = i === s.plan ? 1 : 0;
        if (s.dip[i] > 0) {
          s.dip[i] -= dt;
          target = -0.035;
        }
        const down = target < s.l[i];
        const k = down ? 90 : 62, c = down ? 15 : 9.5;
        s.vl[i] += (k * (target - s.l[i]) - c * s.vl[i]) * dt;
        s.l[i] += s.vl[i] * dt;
        if (s.l[i] < 0) {
          // the rim meets the plate: a small, damped clink
          s.l[i] = 0;
          s.vl[i] = Math.abs(s.vl[i]) * 0.22 < 0.05 ? 0 : Math.abs(s.vl[i]) * 0.22;
        }
        const ta = TILT * clamp(s.l[i], 0, 1.2);
        s.va[i] += (40 * (ta - s.a[i]) - 6.5 * s.va[i] - s.vl[i] * 0.8) * dt;
        s.a[i] = clamp(s.a[i] + s.va[i] * dt, 0, 1.4);
        if (s.l[i] === 0 && s.a[i] < 0.002) s.a[i] = 0;
      });
    };
    const settle = () => {
      plans.forEach((_, i) => {
        s.l[i] = i === s.plan ? 1 : 0;
        s.a[i] = i === s.plan ? TILT : 0;
        s.vl[i] = s.va[i] = s.dip[i] = 0;
      });
    };
    settle();

    let raf = 0, visible = true, last = performance.now(), slow = 0, frames = 0;
    const t0 = last;
    let played = 0, demoAt = 4.0, step = 0;
    const tick = (now: number) => {
      const real = now - last;
      const dt = clamp(real / 1000, 0, 0.05);
      last = now;
      const t = (now - t0) / 1000;
      if (reduce) {
        if (s.dirty) settle();
      } else {
        const sub = Math.max(1, Math.ceil(dt / 0.004));
        for (let i = 0; i < sub; i++) physics(dt / sub);
        played += dt;
        view[0] += (pointer[0] * 0.4 + 0.15 * Math.sin(t * 0.2) - view[0]) * Math.min(1, dt * 3);
        view[1] += (pointer[1] * 0.2 - view[1]) * Math.min(1, dt * 3);
        if (DEMO && !s.touched && played > demoAt) {
          const script = [2, 0, 1];
          setPlan(script[step % script.length]);
          step++;
          demoAt = played + 3.8;
        }
      }
      if (!reduce || s.dirty) {
        draw(reduce ? 0 : t);
        s.dirty = false;
        if (!reduce && ++frames > 20) {
          slow = slow * 0.92 + (real > 24 ? 1 : 0) * 0.08;
          if (slow > 0.6 && scale > 0.5) {
            scale *= 0.84;
            slow = 0;
            frames = 0;
            resize();
          }
        }
      }
      if (visible) raf = requestAnimationFrame(tick);
    };
    const io = new IntersectionObserver(([e]) => {
      const was = visible;
      visible = e.isIntersecting;
      if (visible && !was) {
        last = performance.now();
        frames = 0;
        raf = requestAnimationFrame(tick);
      }
    });
    io.observe(wrap);
    const onVis = () => {
      if (document.hidden) cancelAnimationFrame(raf);
      else if (visible) {
        last = performance.now();
        raf = requestAnimationFrame(tick);
      }
    };
    document.addEventListener("visibilitychange", onVis);
    raf = requestAnimationFrame(tick);

    // pressing a cloche or its plate chooses that plan
    const over = (e: PointerEvent) => {
      const r = wrap.getBoundingClientRect();
      const x = e.clientX - r.left, y = e.clientY - r.top;
      for (let i = 0; i < plans.length; i++) {
        const p = plans[i], R = PLATE(p.dome);
        const a = project([XS[i] - R, domeH(p.dome) + 0.1 + LIFT * s.l[i], 0]), b = project([XS[i] + R, 0, R]);
        if (x > a[0] && x < b[0] && y > a[1] && y < b[1]) return i;
      }
      return -1;
    };
    const touch = () => {
      s.touched = true;
      setHandled(true);
    };
    const downEv = (e: PointerEvent) => {
      touch();
      const i = over(e);
      if (i >= 0) setPlan(i);
    };
    const move = (e: PointerEvent) => {
      const r = wrap.getBoundingClientRect();
      pointer = [((e.clientX - r.left) / r.width - 0.5) * 2, ((e.clientY - r.top) / r.height - 0.5) * -2];
      canvas.style.cursor = over(e) >= 0 ? "pointer" : "";
    };
    const leave = () => {
      pointer = [0, 0];
    };
    const lost = (e: Event) => {
      e.preventDefault();
      setMode("still");
    };
    canvas.addEventListener("pointerdown", downEv);
    canvas.addEventListener("pointermove", move);
    canvas.addEventListener("pointerleave", leave);
    canvas.addEventListener("webglcontextlost", lost);
    wrap.addEventListener("keydown", touch);
    wrap.addEventListener("focusin", touch);

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      io.disconnect();
      document.removeEventListener("visibilitychange", onVis);
      canvas.removeEventListener("pointerdown", downEv);
      canvas.removeEventListener("pointermove", move);
      canvas.removeEventListener("pointerleave", leave);
      canvas.removeEventListener("webglcontextlost", lost);
      wrap.removeEventListener("keydown", touch);
      wrap.removeEventListener("focusin", touch);
      gl.getExtension("WEBGL_lose_context")?.loseContext();
    };
  }, [reduce]);

  const chosen = plans[plan];
  const check = (
    <svg aria-hidden="true" width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mt-0.5 shrink-0">
      <path d="M4 10.5l4 4 8-9" />
    </svg>
  );
  const cta = (
    <a
      href="#"
      className="inline-flex h-11 items-center gap-3 rounded-full bg-[#1f1b17] pl-5 pr-4 text-[14px] font-semibold text-[#f4f1ec] transition hover:bg-[#3a332c] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1f1b17] focus-visible:ring-offset-2 focus-visible:ring-offset-[#ebe8e2]"
    >
      Book {chosen.name}
      <svg aria-hidden="true" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
        <path d="M4 10h12M11 5l5 5-5 5" />
      </svg>
    </a>
  );

  return (
    <section className="relative overflow-hidden" style={{ fontFamily: GROTESK, background: CLOTH, color: INK }}>
      <div ref={wrapRef} className="relative h-[620px] w-full sm:h-[780px]">
        <canvas ref={canvasRef} aria-hidden="true" className={`absolute inset-0 h-full w-full touch-pan-y ${mode === "still" ? "hidden" : ""}`} />
        {mode === "still" && <Still plan={plan} marks={[24, 50, 77]} />}

        <div className="pointer-events-none absolute inset-x-0 top-0 flex items-center justify-between gap-4 p-5 sm:p-9">
          <p className="flex items-baseline gap-3 text-[#1f1b17]">
            <span className="text-[22px] italic leading-none sm:text-[26px]" style={{ fontFamily: SERIF, letterSpacing: "-0.02em" }}>
              {product.name}
            </span>
            <span className="hidden text-[11px] font-semibold uppercase tracking-[0.24em] text-[#1f1b17]/75 sm:inline">{product.line}</span>
          </p>
          <span className="rounded-full border border-[#1f1b17]/30 bg-[#f7f5f1]/60 px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-[#1f1b17] backdrop-blur-sm">Example prices</span>
        </div>

        <div className="pointer-events-none absolute left-5 top-[70px] text-[#1f1b17] sm:left-9 sm:top-[104px]" aria-live={handled ? "polite" : "off"} >
          <p className="text-[11px] font-semibold uppercase tracking-[0.26em] text-[#1f1b17]/80">
            <span className="tabular-nums">{`0${plan + 1}`}</span>
            <span className="mx-2 opacity-60">/</span>
            {chosen.name}
          </p>
          <h2 className="mt-1 flex items-start pb-1 font-semibold leading-[0.84]" style={{ letterSpacing: "-0.06em" }}>
            <span className="mr-1 mt-[0.12em] text-[34px] font-medium sm:text-[60px]" style={{ letterSpacing: 0 }}>
              $
            </span>
            <span className="overflow-hidden">
              <motion.span
                key={plan}
                initial={reduce ? false : { y: "0.6em", opacity: 0 }}
                animate={{ y: 0, opacity: 1 }}
                transition={{ type: "spring", stiffness: 240, damping: 22, delay: reduce ? 0 : 0.15 }}
                className="block text-[88px] tabular-nums sm:text-[150px]"
              >
                {chosen.price}
              </motion.span>
            </span>
            <span className="ml-3 mt-[0.6em] text-[11px] font-semibold uppercase leading-[1.5] tracking-[0.2em] text-[#1f1b17]/80 sm:ml-4 sm:mt-[1.4em] sm:text-[12px]">
              a month
              <span className="block normal-case tracking-normal" style={{ letterSpacing: "0.01em" }}>
                example price
              </span>
            </span>
          </h2>
          <p className="mt-1 max-w-[19ch] text-[19px] italic leading-tight sm:mt-2 sm:max-w-none sm:text-[25px]" style={{ fontFamily: SERIF, letterSpacing: "-0.01em" }}>
            {chosen.pitch}
          </p>
        </div>

        <div className="absolute right-9 top-[104px] hidden w-[262px] text-[#1f1b17] lg:block">
          <ul className="space-y-2 border-t border-[#1f1b17]/25 pt-3.5 text-[14px] leading-snug">
            {chosen.features.map((f) => (
              <li key={f} className="flex gap-3">
                {check}
                {f}
              </li>
            ))}
          </ul>
          <div className="mt-4" style={{ textShadow: "none" }}>
            <a
              href="#"
              className="inline-flex h-11 items-center gap-3 rounded-full bg-[#1f1b17] pl-5 pr-4 text-[14px] font-semibold text-[#f4f1ec] transition hover:bg-[#3a332c] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1f1b17] focus-visible:ring-offset-2 focus-visible:ring-offset-[#ebe8e2]"
            >
              Book {chosen.name}
              <svg aria-hidden="true" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
                <path d="M4 10h12M11 5l5 5-5 5" />
              </svg>
            </a>
          </div>
        </div>

        <fieldset className="absolute inset-x-0 bottom-4 h-11 sm:bottom-6">
          <legend className="sr-only">Plan</legend>
          {plans.map((p, i) => (
            <label
              key={p.name}
              htmlFor={`silver-cloche-pricing-plan-${i}`}
              className="absolute top-0 -translate-x-1/2 cursor-pointer"
              style={{ left: `${mode === "still" ? [24, 50, 77][i] : marks[i]}%` }}
            >
              <input id={`silver-cloche-pricing-plan-${i}`} type="radio" name="silver-cloche-pricing-plan" value={p.name} checked={plan === i} onChange={() => setPlan(i)} className="peer sr-only" />
              <span className="flex h-11 items-center gap-2 whitespace-nowrap rounded-full border border-[#1f1b17]/15 bg-[#f7f5f1]/90 pl-2.5 pr-3 text-[11px] font-semibold uppercase tracking-[0.08em] shadow-[0_6px_18px_rgba(40,30,20,0.16)] transition peer-checked:bg-[#1f1b17] peer-checked:text-[#f4f1ec] peer-focus-visible:ring-2 peer-focus-visible:ring-[#1f1b17] peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-[#f7f5f1] sm:gap-2.5 sm:pl-3.5 sm:pr-4 sm:text-[13px] sm:tracking-[0.14em]">
                <span className={`grid h-4 w-4 place-items-center rounded-full border-[1.5px] ${plan === i ? "border-[#f4f1ec]" : "border-[#1f1b17]/60"}`}>
                  <span className={`h-2 w-2 rounded-full bg-[#f4f1ec] transition-transform ${plan === i ? "scale-100" : "scale-0"}`} />
                </span>
                {p.name}
                <span className="hidden font-medium normal-case tracking-normal opacity-80 sm:inline">${p.price}</span>
              </span>
            </label>
          ))}
        </fieldset>
      </div>

      <div className="border-t border-[#1f1b17]/15 px-5 pb-8 pt-6 lg:hidden">
        <ul className="space-y-2.5 text-[15px] leading-snug">
          {chosen.features.map((f) => (
            <li key={f} className="flex gap-3">
              {check}
              {f}
            </li>
          ))}
        </ul>
        <div className="mt-6">{cta}</div>
        <p className="mt-4 text-[13px] text-[#1f1b17]/80">Prices shown are examples. Set your own in the plans list.</p>
      </div>
    </section>
  );
}
```
