{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"client-plates","type":"registry:block","title":"Engraved Client Plates","description":"Block. “Used in production by…”, as metal name plates. Brushed aluminium or anodised black, screwed or riveted, each client’s mark cut into the metal with one quiet number under it.","registryDependencies":[],"files":[{"path":"tactile/client-plates/client-plates.mjs","type":"registry:file","target":"tactile/client-plates/client-plates.mjs","content":"/**\n * Engraved Client Plates — TACTILE UI block, cut 2 (00.2).\n *\n * \"Used in production by…\": the clients of an agent-infrastructure product\n * as small metal name plates fixed to the desk — each with four slotted\n * screws or two rivets, a chamfered edge, and the company's mark and name CUT\n * INTO the plate, not printed on it. On aluminium the cut is filled with dark\n * enamel; on graphite the plate is anodised black and the cut shows bright\n * metal. Under each plate, optionally, one quiet number.\n *\n * Mechanism: CSS and inline SVG. The engraving is one drop-shadow pair on the\n * whole engraved group (mark and type alike): a dark line on the upper wall of\n * every cut, a lit lip on the lower one — the lamp is fixed above left. The\n * screws' slots are turned the way a fitter leaves them (a stable angle from\n * the company's name); their sheen does not turn with them.\n *\n * Motion: static by default. `motion: 'drift'` sets the plates on a slow belt\n * in a recessed channel (a duplicated track, translateX only), with a Pause\n * key; it pauses on hover, on focus, off screen, and never runs under reduced\n * motion, where the plates stand in their grid.\n *\n * Accessibility: a section named by its h2; the plates are a list; every\n * name is real text (the marks and the screws are hidden); the drifting\n * duplicate is aria-hidden and inert.\n */\n\n/** The clients, invented for this block: believable companies from the product's world. */\nexport const CLIENTS = Object.freeze([\n  Object.freeze({ id: 'fenmoor', name: 'Fenmoor', kind: 'Energy co-op', type: 'caps', fix: 'screws', mark: 'fen', stat: '4,100 meters read every hour' }),\n  Object.freeze({ id: 'allard', name: 'Allard', kind: 'Robotics lab', type: 'caps', fix: 'rivets', mark: 'hex', stat: '96 robot cells on one scheduler' }),\n  Object.freeze({ id: 'lading', name: 'Lading', kind: 'Freight API', type: 'title', fix: 'screws', mark: 'crates', stat: '2.3 M shipments routed a month' }),\n  Object.freeze({ id: 'understudy', name: 'understudy', kind: 'Agent studio', type: 'lower', fix: 'rivets', mark: 'pair', stat: '312 agents in production' }),\n  Object.freeze({ id: 'northmere', name: 'Northmere', kind: 'Water telemetry', type: 'caps', fix: 'screws', mark: 'drop', stat: '11,400 sensors on one bus' }),\n  Object.freeze({ id: 'tallyman', name: 'Tallyman', kind: 'Ledger API', type: 'title', fix: 'rivets', mark: 'tally', stat: '£1.2 bn reconciled nightly' }),\n  Object.freeze({ id: 'cairnfield', name: 'Cairnfield', kind: 'Solar farms', type: 'caps', fix: 'screws', mark: 'cairn', stat: '38 MW behind one dashboard' }),\n  Object.freeze({ id: 'seastack', name: 'seastack', kind: 'Edge inference', type: 'lower', fix: 'rivets', mark: 'stacks', stat: '40 edge sites, one fleet' }),\n]);\n\n/** Engraved marks on a 32 grid: strokes and fills are both cut, so both read the same in either finish. */\nexport const MARKS = Object.freeze({\n  fen: '<path d=\"M7 17a9 9 0 0 1 18 0z\" fill=\"currentColor\"/><path d=\"M4 21h24M8 25h16\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" fill=\"none\"/>',\n  hex: '<path d=\"M16 3.5 27 9.75v12.5L16 28.5 5 22.25V9.75z\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linejoin=\"round\" fill=\"none\"/><path d=\"M11 20.5l5-4.5 5 4.5M16 16v-5.5\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" fill=\"none\"/>',\n  crates: '<rect x=\"4\" y=\"16\" width=\"11\" height=\"11\" rx=\"1\" fill=\"currentColor\"/><rect x=\"17\" y=\"16\" width=\"11\" height=\"11\" rx=\"1\" fill=\"currentColor\"/><rect x=\"10.5\" y=\"4\" width=\"11\" height=\"10\" rx=\"1\" fill=\"currentColor\"/>',\n  pair: '<circle cx=\"12\" cy=\"16\" r=\"8.5\" stroke=\"currentColor\" stroke-width=\"2.4\" fill=\"none\"/><circle cx=\"20\" cy=\"16\" r=\"8.5\" fill=\"currentColor\"/>',\n  drop: '<path d=\"M16 4.5c4.8 6.2 7.5 10.5 7.5 14a7.5 7.5 0 0 1-15 0c0-3.5 2.7-7.8 7.5-14z\" fill=\"currentColor\"/><path d=\"M4 28h24\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\"/>',\n  tally: '<path d=\"M7 6v20M12 6v20M17 6v20M22 6v20M4 22 26 9\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" fill=\"none\"/>',\n  cairn: '<ellipse cx=\"16\" cy=\"24.6\" rx=\"10.5\" ry=\"3.6\" fill=\"currentColor\"/><ellipse cx=\"14.8\" cy=\"17.4\" rx=\"7.2\" ry=\"3.1\" fill=\"currentColor\"/><ellipse cx=\"16.6\" cy=\"11\" rx=\"4.4\" ry=\"2.5\" fill=\"currentColor\"/><circle cx=\"25\" cy=\"6.5\" r=\"2.6\" fill=\"currentColor\"/>',\n  stacks: '<path d=\"M7 27V12M13 27V6M19 27V14M25 27V19\" stroke=\"currentColor\" stroke-width=\"3.4\" stroke-linecap=\"round\"/><path d=\"M3 27.5h26\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>',\n});\n\nexport const FINISHES = Object.freeze(['auto', 'aluminium', 'graphite']);\nexport const MOTIONS = Object.freeze(['static', 'drift']);\nexport const FIXINGS = Object.freeze(['mixed', 'screws', 'rivets']);\n\nexport const DEFAULTS = Object.freeze({\n  label: 'Customers',\n  title: 'Used in production by',\n  note: '1,940 teams run agents on Lode, from co-ops to banks.',\n  count: 6,\n  stats: true,\n  fixing: 'mixed',\n  motion: 'static',\n  speed: 40,\n  finish: 'auto',\n});\n\n/** A stable small number from a string: the same plate is always fitted the same way. */\nexport function hash(s) {\n  let h = 2166136261;\n  for (const ch of String(s)) { h ^= ch.codePointAt(0); h = Math.imul(h, 16777619); }\n  return h >>> 0;\n}\n\n/** How a fitter left the four slots: angles in degrees, stable per plate, never all alike. */\nexport function slotAngles(name, n = 4) {\n  const h = hash(name);\n  return Array.from({ length: n }, (_, i) => ((h >>> (i * 7)) % 180) - 90);\n}\n\n/**\n * Type sizes on a plate are shares of its width (cqi). The advance of each\n * style, per character with its tracking, in ems of Instrument Sans Bold:\n * enough to fit a name to the room between the mark and the far fixing.\n */\nexport const ADVANCE = Object.freeze({ caps: 0.87, title: 0.58, lower: 0.56, kind: 0.84 });\nexport const BASE_SIZE = Object.freeze({ caps: 9.6, title: 12, lower: 12, kind: 4 });\n/** The room for the name, in cqi: 100 − 2 × 12 inset − 17 mark − 5 gap. */\nexport const ROOM = 54;\n\n/** How much a line of engraving must shrink to fit: 1 when it fits already. */\nexport function fitScale(text, style, room = ROOM) {\n  const w = [...String(text)].length * ADVANCE[style] * BASE_SIZE[style];\n  return w <= room ? 1 : Math.max(0.5, Math.floor((room / w) * 1000) / 1000);\n}\n\n/** The fixing a plate gets. */\nexport const fixingFor = (client, fixing) => (fixing === 'mixed' ? client.fix : fixing);\n\n/** Seconds for one lap of the belt: the same pace (px/s) whatever the count. */\nexport const lapSeconds = (count, speed) => Math.round((count * 260) / Math.max(10, speed));\n\n/** Columns per composition: one row where it fits, else a grid. */\nexport function columns(count, width) {\n  if (width >= 1000) return count <= 6 ? count : Math.ceil(count / 2);\n  if (width >= 600) return count === 4 || count === 8 ? 4 : 3;\n  return 2;\n}\n\nexport function normalise(opts = {}, prev = DEFAULTS) {\n  const o = { ...prev, ...opts };\n  for (const k of ['label', 'title', 'note']) o[k] = String(o[k] ?? '');\n  o.count = Math.min(CLIENTS.length, Math.max(4, Math.round(Number(o.count)) || DEFAULTS.count));\n  o.stats = o.stats !== false && o.stats !== 'false';\n  if (!FIXINGS.includes(o.fixing)) o.fixing = DEFAULTS.fixing;\n  if (!MOTIONS.includes(o.motion)) o.motion = DEFAULTS.motion;\n  o.speed = Math.min(80, Math.max(10, Math.round(Number(o.speed)) || DEFAULTS.speed));\n  if (!FINISHES.includes(o.finish)) o.finish = 'auto';\n  return o;\n}\n\nexport function resolveFinish(own, around, prefersDark, tone) {\n  if (own === 'aluminium' || own === 'graphite') return own;\n  if (around === 'aluminium' || around === 'graphite') return around;\n  // Any other finish around it (another family's: porcelain, black) by its tone.\n  if (tone === 'light' || tone === 'dark') return tone === 'dark' ? 'graphite' : 'aluminium';\n  return prefersDark ? 'graphite' : 'aluminium';\n}\n\nconst esc = (s) => String(s).replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' })[c]);\n\n/** A slotted screw: the head and its sheen stay put; only the slot is turned. */\nconst screw = (deg) => `<svg class=\"cpl-screw\" viewBox=\"0 0 12 12\" aria-hidden=\"true\"><circle cx=\"6\" cy=\"6\" r=\"5.4\" class=\"cpl-sink\"/><circle cx=\"6\" cy=\"6\" r=\"4.3\" class=\"cpl-head\"/><ellipse cx=\"4.6\" cy=\"4.3\" rx=\"2\" ry=\"1.4\" class=\"cpl-shine\"/><g transform=\"translate(.3 .45)\"><path d=\"M2.7 6h6.6\" class=\"cpl-slot-lip\" transform=\"rotate(${deg} 6 6)\"/></g><path d=\"M2.7 6h6.6\" class=\"cpl-slot\" transform=\"rotate(${deg} 6 6)\"/></svg>`;\nconst rivet = () => '<svg class=\"cpl-rivet\" viewBox=\"0 0 12 12\" aria-hidden=\"true\"><circle cx=\"6\" cy=\"6\" r=\"5.3\" class=\"cpl-sink\"/><circle cx=\"6\" cy=\"6\" r=\"4.5\" class=\"cpl-dome\"/><circle cx=\"5.55\" cy=\"5.5\" r=\"4\" class=\"cpl-dome-lo\"/><ellipse cx=\"4.7\" cy=\"4.5\" rx=\"1.9\" ry=\"1.4\" class=\"cpl-shine\"/></svg>';\n\nfunction plateHtml(c, o, uid) {\n  const fix = fixingFor(c, o.fixing);\n  const a = slotAngles(c.name);\n  const fixings = fix === 'screws'\n    ? `<span class=\"cpl-fix cpl-fix--tl\">${screw(a[0])}</span><span class=\"cpl-fix cpl-fix--tr\">${screw(a[1])}</span><span class=\"cpl-fix cpl-fix--bl\">${screw(a[2])}</span><span class=\"cpl-fix cpl-fix--br\">${screw(a[3])}</span>`\n    : `<span class=\"cpl-fix cpl-fix--l\">${rivet()}</span><span class=\"cpl-fix cpl-fix--r\">${rivet()}</span>`;\n  return `<div class=\"cpl-plate\" data-fix=\"${fix}\" data-type=\"${c.type}\" style=\"--fit: ${fitScale(c.name, c.type)}; --fit-kind: ${fitScale(c.kind, 'kind')}\">\n      ${fixings}\n      <div class=\"cpl-cut\">\n        <svg class=\"cpl-mark\" viewBox=\"0 0 32 32\" aria-hidden=\"true\">${MARKS[c.mark]}</svg>\n        <p class=\"cpl-name\" id=\"${uid}-${c.id}\"><span class=\"cpl-word\">${esc(c.name)}</span><span class=\"cpl-kind\">${esc(c.kind)}</span></p>\n      </div>\n    </div>`;\n}\n\n/**\n * The cut, as one SVG filter: a band of shadow along the upper-left wall\n * inside every stroke (the wall that faces away from the lamp) and a lit lip\n * just below-right of it (the wall that faces the lamp). Flood colours come\n * from the finish's tokens, so the same filter cuts both plates.\n */\nconst engraveDefs = (uid) => `<svg class=\"cpl-defs\" aria-hidden=\"true\" focusable=\"false\"><filter id=\"${uid}-cut\" x=\"-4%\" y=\"-12%\" width=\"108%\" height=\"130%\" color-interpolation-filters=\"sRGB\">\n  <feOffset in=\"SourceAlpha\" dx=\"0.35\" dy=\"0.8\" result=\"down\"/>\n  <feFlood class=\"cpl-f-lip\" result=\"lipColour\"/>\n  <feComposite in=\"lipColour\" in2=\"down\" operator=\"in\" result=\"lip\"/>\n  <feOffset in=\"SourceAlpha\" dx=\"0.5\" dy=\"1.1\" result=\"shifted\"/>\n  <feComposite in=\"SourceAlpha\" in2=\"shifted\" operator=\"out\" result=\"upperWall\"/>\n  <feFlood class=\"cpl-f-wall\" result=\"wallColour\"/>\n  <feComposite in=\"wallColour\" in2=\"upperWall\" operator=\"in\" result=\"wall\"/>\n  <feMerge><feMergeNode in=\"lip\"/><feMergeNode in=\"SourceGraphic\"/><feMergeNode in=\"wall\"/></feMerge>\n</filter></svg>`;\n\nlet seq = 0;\n\n/**\n * @param {HTMLElement} host\n * @param {object} opts see DEFAULTS\n * @returns {{ root: HTMLElement, update(opts: object): void, destroy(): void, readonly paused: boolean }}\n */\nexport function mount(host, opts = {}) {\n  let o = normalise(opts);\n  const uid = `cpl${(seq += 1)}`;\n  const root = document.createElement('section');\n  root.className = 'cpl';\n  root.dataset.block = 'client-plates';\n  root.setAttribute('aria-labelledby', `${uid}-title`);\n  host.append(root);\n  let paused = false;\n\n  const render = () => {\n    const list = CLIENTS.slice(0, o.count);\n    const items = (dup) => list.map((c) => `<li class=\"cpl-item\">\n    ${plateHtml(c, o, dup ? `${uid}d` : uid)}\n    ${o.stats ? `<p class=\"cpl-stat\">${esc(c.stat)}</p>` : ''}\n  </li>`).join('');\n    root.dataset.count = String(o.count);\n    root.dataset.motion = o.motion;\n    root.dataset.stats = String(o.stats);\n    root.dataset.paused = String(paused);\n    root.style.setProperty('--cpl-lap', `${lapSeconds(o.count, o.speed)}s`);\n    root.style.setProperty('--cpl-engrave', `url(#${uid}-cut)`);\n    root.style.setProperty('--cpl-cols-wide', String(columns(o.count, 1000)));\n    root.style.setProperty('--cpl-cols-mid', String(columns(o.count, 600)));\n    const drift = o.motion === 'drift';\n    root.innerHTML = `${engraveDefs(uid)}<div class=\"cpl-wrap\">\n  <header class=\"cpl-head\">\n    <div>\n      <p class=\"cpl-label\"></p>\n      <h2 class=\"cpl-title\" id=\"${uid}-title\"></h2>\n    </div>\n    <div class=\"cpl-side\">\n      <p class=\"cpl-note\"></p>\n      ${drift ? `<button type=\"button\" class=\"cpl-key\" data-f=\"pause\" aria-pressed=\"${paused}\">${paused ? 'Resume' : 'Pause'}</button>` : ''}\n    </div>\n  </header>\n  <div class=\"cpl-belt\"><div class=\"cpl-track\">\n    <ul class=\"cpl-list\" role=\"list\">${items(false)}</ul>${drift ? `<ul class=\"cpl-list cpl-list--dup\" role=\"list\" aria-hidden=\"true\" inert>${items(true)}</ul>` : ''}\n  </div></div>\n</div>`;\n    root.querySelector('.cpl-label').textContent = o.label;\n    root.querySelector('.cpl-title').textContent = o.title;\n    root.querySelector('.cpl-note').textContent = o.note;\n  };\n\n  const onClick = (e) => {\n    const key = e.target.closest('[data-f=\"pause\"]');\n    if (!key) return;\n    paused = !paused;\n    root.dataset.paused = String(paused);\n    key.setAttribute('aria-pressed', String(paused));\n    key.textContent = paused ? 'Resume' : 'Pause';\n  };\n  root.addEventListener('click', onClick);\n\n  const dark = typeof matchMedia === 'function' ? matchMedia('(prefers-color-scheme: dark)') : null;\n  const applyFinish = () => {\n    const f = resolveFinish(o.finish, host.closest('[data-finish]')?.dataset.finish, dark?.matches, host.closest('[data-tone=\"light\"], [data-tone=\"dark\"]')?.dataset.tone);\n    if (root.dataset.finish !== f) root.dataset.finish = f;\n  };\n  const finishObs = new MutationObserver(applyFinish);\n  finishObs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-finish'], subtree: true });\n  dark?.addEventListener('change', applyFinish);\n\n  const io = typeof IntersectionObserver === 'function'\n    ? new IntersectionObserver(([e]) => { root.dataset.visible = String(e.isIntersecting); })\n    : null;\n  io?.observe(root);\n\n  render();\n  applyFinish();\n\n  return {\n    root,\n    get paused() { return paused; },\n    update(next = {}) {\n      o = normalise(next, o);\n      render();\n      applyFinish();\n    },\n    destroy() {\n      root.removeEventListener('click', onClick);\n      finishObs.disconnect();\n      dark?.removeEventListener('change', applyFinish);\n      io?.disconnect();\n      root.remove();\n    },\n  };\n}\n\nexport default { mount };\n"},{"path":"tactile/client-plates/client-plates.css","type":"registry:file","target":"tactile/client-plates/client-plates.css","content":"/* Engraved Client Plates — TACTILE UI block, cut 2 (00.2). Everything under .cpl.\n   Face: Instrument Sans (SIL OFL 1.1), shipped beside this file with its\n   licence. The tokens are the Tactile UI Kit's own names and values, defined\n   here so the block needs nothing else; client-plates.mjs writes the resolved\n   finish on the root. */\n\n@font-face { font-family: 'CP Instrument Sans'; src: url('./InstrumentSans-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }\n@font-face { font-family: 'CP Instrument Sans'; src: url('./InstrumentSans-Bold.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }\n\n/* ------------------------------------------------------------------ tokens */\n.cpl {\n  color-scheme: light;\n  --tui-desk: #d8d7d2;\n  --tui-face-hi: #f2f2ef;\n  --tui-well: #d3d4d0;\n  --tui-skirt: #a4a5a1;\n  --tui-ink: #1b1c1e;\n  --tui-ink-2: #4b4d51;\n  --tui-rule: rgb(27 28 30 / 0.16);\n  --tui-hl: rgb(255 255 255 / 0.9);\n  --tui-sh: 24 25 28;\n  --tui-sh-a: 0.24;\n  --tui-focus: #1b1c1e;\n  --tui-ease: cubic-bezier(0.2, 0.7, 0.2, 1);\n  --tui-well-fill: linear-gradient(to bottom, color-mix(in oklab, var(--tui-well) 86%, rgb(var(--tui-sh))), var(--tui-well) 55%);\n  --tui-inset:\n    inset 0 3px 3px -2px rgb(var(--tui-sh) / calc(var(--tui-sh-a) * 2.6)),\n    inset 0 9px 12px -9px rgb(var(--tui-sh) / calc(var(--tui-sh-a) * 1.8)),\n    inset 0 0 0 1px rgb(var(--tui-sh) / calc(var(--tui-sh-a) * 0.45)),\n    inset 0 -1px 0 color-mix(in oklab, var(--tui-hl) 60%, transparent),\n    0 1px 0 var(--tui-hl),\n    0 -1px 0 rgb(var(--tui-sh) / calc(var(--tui-sh-a) * 0.35));\n  --tui-pressed:\n    inset 0 5px 5px -4px rgb(var(--tui-sh) / calc(var(--tui-sh-a) * 3)),\n    inset 4px 0 4px -4px rgb(var(--tui-sh) / calc(var(--tui-sh-a) * 1.4)),\n    inset -4px 0 4px -4px rgb(var(--tui-sh) / calc(var(--tui-sh-a) * 1.4)),\n    0 -1px 0 rgb(var(--tui-sh) / calc(var(--tui-sh-a) * 1.3)),\n    0 1px 0 var(--tui-hl);\n  --tui-noise: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 0.28 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\");\n  --cpl-brush: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='120'%3E%3Cfilter id='b' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.0035 .9' numOctaves='2' seed='4' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .5 0 0 0 0 .5 0 0 0 0 .5 1.6 0 0 0 -.62'/%3E%3C/filter%3E%3Crect width='300' height='120' filter='url(%23b)'/%3E%3C/svg%3E\");\n  --cpl-brush-dark: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='120'%3E%3Cfilter id='b' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.0035 .9' numOctaves='2' seed='4' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .5 0 0 0 0 .5 0 0 0 0 .5 1.1 0 0 0 -.5'/%3E%3C/filter%3E%3Crect width='300' height='120' filter='url(%23b)'/%3E%3C/svg%3E\");\n\n  /* The plate: brushed aluminium, the cut filled with dark enamel. */\n  --cpl-plate-hi: #eeeeea;\n  --cpl-plate: #dcdcd7;\n  --cpl-plate-lo: #c6c7c2;\n  --cpl-chamfer: linear-gradient(135deg, rgb(255 255 255 / 0.95), rgb(255 255 255 / 0.35) 30%, rgb(0 0 0 / 0.06) 55%, rgb(0 0 0 / 0.32));\n  --cpl-cut: #26272a;\n  --cpl-cut-2: #3d3e42;\n  --cpl-lip: rgb(255 255 255 / 0.92);\n  --cpl-wall: rgb(0 0 0 / 0.55);\n  --cpl-steel: #d9dad6;\n  --cpl-steel-lo: #8e8f8b;\n  --cpl-sans: 'CP Instrument Sans', 'Instrument Sans', ui-sans-serif, system-ui, sans-serif;\n\n  position: relative;\n  container: cpl / inline-size;\n  background: var(--tui-desk);\n  color: var(--tui-ink);\n  font: 400 15px/1.55 var(--cpl-sans);\n  font-feature-settings: 'tnum' 1;\n  -webkit-font-smoothing: antialiased;\n}\n/* Graphite: anodised black, the cut and the chamfer showing bright metal. */\n.cpl[data-finish='graphite'] {\n  color-scheme: dark;\n  --tui-desk: #0e0f10;\n  --tui-face-hi: #28292c;\n  --tui-well: #121315;\n  --tui-skirt: #09090a;\n  --tui-ink: #e8e6e1;\n  --tui-ink-2: #a6a7a9;\n  --tui-rule: rgb(232 230 225 / 0.11);\n  --tui-hl: rgb(255 255 255 / 0.11);\n  --tui-sh: 0 0 0;\n  --tui-sh-a: 0.55;\n  --tui-focus: #e8e6e1;\n  --tui-well-fill: linear-gradient(to bottom, #070808, var(--tui-well) 45%, #17181a);\n  --cpl-brush: var(--cpl-brush-dark);\n  --cpl-plate-hi: #2a2b2e;\n  --cpl-plate: #1c1d1f;\n  --cpl-plate-lo: #141517;\n  --cpl-chamfer: linear-gradient(135deg, rgb(236 237 233 / 0.95), rgb(200 201 197 / 0.6) 30%, rgb(120 121 118 / 0.55) 60%, rgb(70 71 69 / 0.8));\n  --cpl-cut: #d0d2cd;\n  --cpl-cut-2: #a9aba6;\n  --cpl-lip: rgb(255 255 255 / 0.16);\n  --cpl-wall: rgb(0 0 0 / 0.62);\n  --cpl-steel: #c7c8c4;\n  --cpl-steel-lo: #6a6b68;\n}\n.cpl *, .cpl *::before, .cpl *::after { box-sizing: border-box; }\n\n/* ------------------------------------------------------------------ layout */\n.cpl-wrap {\n  width: min(100% - 2 * clamp(20px, 5cqi, 64px), 1240px);\n  margin-inline: auto;\n  padding-block: clamp(48px, 7cqi, 104px);\n}\n.cpl-head {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: end;\n  justify-content: space-between;\n  gap: 16px 48px;\n  margin-bottom: clamp(28px, 4cqi, 52px);\n  padding-bottom: 18px;\n  border-bottom: 1px solid var(--tui-rule);\n}\n.cpl-label {\n  margin: 0 0 12px;\n  font: 700 10.5px/1.2 var(--cpl-sans);\n  letter-spacing: 0.09em;\n  text-transform: uppercase;\n  color: var(--tui-ink-2);\n}\n.cpl-title {\n  margin: 0;\n  font: 700 clamp(26px, 2.8cqi, 36px)/1.05 var(--cpl-sans);\n  letter-spacing: -0.03em;\n}\n.cpl-side { display: flex; align-items: center; gap: 14px; }\n.cpl-note { margin: 0; max-width: 40ch; font-size: 14.5px; color: var(--tui-ink-2); text-wrap: pretty; }\n\n.cpl-list {\n  display: grid;\n  grid-template-columns: repeat(var(--cpl-cols-wide, 6), minmax(0, 1fr));\n  gap: clamp(18px, 2.2cqi, 32px) clamp(14px, 1.8cqi, 26px);\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n.cpl-item { min-width: 0; }\n.cpl-stat {\n  margin: 12px 0 0;\n  padding-left: 2px;\n  font-size: 13px;\n  line-height: 1.4;\n  color: var(--tui-ink-2);\n  text-wrap: balance;\n}\n\n/* ------------------------------------------------------------------ the plate */\n.cpl-plate {\n  container-type: inline-size;\n  position: relative;\n  isolation: isolate;\n  aspect-ratio: 2.3 / 1;\n  border-radius: 6px;\n  background:\n    linear-gradient(160deg, rgb(255 255 255 / 0.22), transparent 38%, rgb(0 0 0 / 0.07)),\n    var(--cpl-brush) 0 0 / 300px 120px,\n    var(--tui-noise),\n    linear-gradient(180deg, var(--cpl-plate-hi), var(--cpl-plate) 55%, var(--cpl-plate-lo));\n  /* A thin plate: a short, soft shadow down and to the right. */\n  box-shadow:\n    0 0 0 0.5px rgb(var(--tui-sh) / calc(var(--tui-sh-a) * 1.4)),\n    0 1px 1px rgb(var(--tui-sh) / calc(var(--tui-sh-a) * 1.3)),\n    1px 5px 10px -4px rgb(var(--tui-sh) / calc(var(--tui-sh-a) * 1.6));\n}\n/* The chamfer: a 45° edge machined all round, catching the lamp top-left. */\n.cpl-plate::before {\n  content: '';\n  position: absolute;\n  inset: 0;\n  z-index: -1;\n  border-radius: inherit;\n  padding: max(1.5px, 1.1cqi);\n  background: var(--cpl-chamfer);\n  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);\n  -webkit-mask-composite: xor;\n  mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);\n}\n\n/* Fixings, set in from the corners. */\n.cpl-fix { position: absolute; width: max(8px, 6.2cqi); aspect-ratio: 1; line-height: 0; }\n.cpl-fix svg { width: 100%; height: 100%; overflow: visible; }\n.cpl-fix--tl { left: 4.6cqi; top: 4.6cqi; }\n.cpl-fix--tr { right: 4.6cqi; top: 4.6cqi; }\n.cpl-fix--bl { left: 4.6cqi; bottom: 4.6cqi; }\n.cpl-fix--br { right: 4.6cqi; bottom: 4.6cqi; }\n.cpl-fix--l { left: 4.2cqi; top: 50%; translate: 0 -50%; }\n.cpl-fix--r { right: 4.2cqi; top: 50%; translate: 0 -50%; }\n.cpl-sink { fill: rgb(var(--tui-sh) / calc(var(--tui-sh-a) * 1.5)); }\n.cpl-head { fill: var(--cpl-steel); }\n.cpl-shine { fill: rgb(255 255 255 / 0.85); }\n.cpl-slot { stroke: rgb(0 0 0 / 0.72); stroke-width: 1.15; stroke-linecap: round; }\n.cpl-slot-lip { stroke: rgb(255 255 255 / 0.7); stroke-width: 0.7; stroke-linecap: round; }\n.cpl-dome { fill: var(--cpl-steel-lo); }\n.cpl-dome-lo { fill: var(--cpl-steel); }\n.cpl-screw, .cpl-rivet { filter: drop-shadow(0.4px 0.7px 0.5px rgb(0 0 0 / 0.45)); }\n\n/* The cut: mark and name engraved alike — a shadow on the upper wall of\n   every cut and a lit lip on the lower one. The small line of type is cut\n   shallower: only its lip shows, or its thin strokes would be all wall. */\n.cpl-cut {\n  position: absolute;\n  inset: 0 12cqi;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 5cqi;\n  color: var(--cpl-cut);\n}\n/* The engrave filter is an SVG filter in the section (its id in --cpl-engrave). */\n.cpl-mark, .cpl-word { filter: var(--cpl-engrave); }\n.cpl-kind { filter: drop-shadow(0 0.6px 0 var(--cpl-lip)); }\n.cpl-defs { position: absolute; width: 0; height: 0; overflow: hidden; }\n.cpl-defs feFlood.cpl-f-lip { flood-color: var(--cpl-lip); }\n.cpl-defs feFlood.cpl-f-wall { flood-color: var(--cpl-wall); }\n.cpl-plate[data-fix='rivets'] .cpl-cut { inset: 0 13cqi; }\n.cpl-mark { flex: none; width: 17cqi; height: auto; }\n.cpl-name { display: grid; gap: 1.6cqi; min-width: 0; margin: 0; }\n.cpl-word {\n  font: 700 calc(12cqi * var(--fit, 1))/0.95 var(--cpl-sans);\n  letter-spacing: -0.02em;\n  white-space: nowrap;\n}\n.cpl-plate[data-type='caps'] .cpl-word { font-size: calc(9.6cqi * var(--fit, 1)); letter-spacing: 0.12em; text-transform: uppercase; margin-right: -0.12em; }\n.cpl-plate[data-type='lower'] .cpl-word { letter-spacing: -0.035em; }\n.cpl-kind {\n  font: 700 max(7px, calc(4cqi * var(--fit-kind, 1)))/1.1 var(--cpl-sans);\n  letter-spacing: 0.16em;\n  text-transform: uppercase;\n  color: var(--cpl-cut-2);\n  white-space: nowrap;\n}\n\n/* ------------------------------------------------------------------ drift: a belt in a recessed channel */\n.cpl[data-motion='drift'] .cpl-belt {\n  overflow: hidden;\n  padding: clamp(18px, 2.4cqi, 28px) 0;\n  border-radius: 12px;\n  background: var(--tui-well-fill) var(--tui-well);\n  box-shadow: var(--tui-inset);\n}\n.cpl[data-motion='drift'] .cpl-track {\n  display: flex;\n  width: max-content;\n  animation: cpl-drift var(--cpl-lap, 40s) linear infinite;\n}\n.cpl[data-motion='drift'] .cpl-list {\n  display: flex;\n  gap: 0;\n}\n.cpl[data-motion='drift'] .cpl-item { width: clamp(168px, 17cqi, 210px); margin-left: clamp(16px, 2cqi, 28px); }\n.cpl[data-motion='drift'][data-paused='true'] .cpl-track,\n.cpl[data-motion='drift'][data-visible='false'] .cpl-track,\n.cpl[data-motion='drift'] .cpl-belt:hover .cpl-track,\n.cpl[data-motion='drift'] .cpl-belt:focus-within .cpl-track { animation-play-state: paused; }\n@keyframes cpl-drift { to { transform: translateX(-50%); } }\n\n.cpl-key {\n  appearance: none;\n  flex: none;\n  height: 28px;\n  padding: 0 12px;\n  border: 0;\n  border-radius: 6px;\n  font: 700 11.5px/1 var(--cpl-sans);\n  color: var(--tui-ink);\n  cursor: pointer;\n  background:\n    radial-gradient(120% 90% at 50% 0%, var(--tui-hl), transparent 60%),\n    linear-gradient(var(--tui-face-hi), color-mix(in oklab, var(--tui-face-hi) 92%, rgb(var(--tui-sh))));\n  box-shadow: inset 0 1px 0 var(--tui-hl), 0 1.5px 0 var(--tui-skirt), 0 2.5px 3px rgb(var(--tui-sh) / 0.3);\n  transition: transform 70ms var(--tui-ease), box-shadow 70ms var(--tui-ease);\n}\n.cpl-key:active, .cpl-key[aria-pressed='true'] { transform: translateY(1.5px); box-shadow: var(--tui-pressed); }\n.cpl :focus-visible { outline: 2px solid var(--tui-focus); outline-offset: 3px; }\n\n/* ------------------------------------------------------------------ re-composed by the width it is given */\n@container cpl (max-width: 999px) {\n  .cpl-list { grid-template-columns: repeat(var(--cpl-cols-mid, 3), minmax(0, 1fr)); }\n}\n@container cpl (max-width: 599px) {\n  .cpl-head { flex-direction: column; align-items: flex-start; gap: 10px; }\n  .cpl-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px 14px; }\n  .cpl-stat { font-size: 12.5px; margin-top: 10px; }\n  .cpl[data-motion='drift'] .cpl-item { width: 156px; margin-left: 14px; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  /* No belt: the plates stand in their grid, and the copy on the belt goes. */\n  .cpl[data-motion='drift'] .cpl-belt { overflow: visible; padding: 0; background: none; box-shadow: none; border-radius: 0; }\n  .cpl[data-motion='drift'] .cpl-track { display: block; width: auto; animation: none; }\n  .cpl[data-motion='drift'] .cpl-list { display: grid; gap: clamp(18px, 2.2cqi, 32px) clamp(14px, 1.8cqi, 26px); }\n  .cpl[data-motion='drift'] .cpl-list--dup { display: none; }\n  .cpl[data-motion='drift'] .cpl-item { width: auto; margin-left: 0; }\n  .cpl-key[data-f='pause'] { display: none; }\n  .cpl-key { transition: none; }\n}\n"},{"path":"tactile/client-plates/InstrumentSans-OFL.txt","type":"registry:file","target":"tactile/client-plates/InstrumentSans-OFL.txt","content":"Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"},{"path":"tactile/client-plates/example.html","type":"registry:file","target":"tactile/client-plates/example.html","content":"<!doctype html>\n<html lang=\"en\">\n<!-- Engraved Client Plates v0.1.0: Tactile UI\n     Put this file beside the folder and serve them over HTTP (any static\n     server that knows .woff2):\n       ./client-plates/   client-plates.mjs, client-plates.css, InstrumentSans-Regular.woff2, InstrumentSans-Bold.woff2, InstrumentSans-OFL.txt\n     No framework, no build step, no image: the plates, marks and screws are\n     CSS and inline SVG. The clients are in CLIENTS in client-plates.mjs:\n     put your own there (name, kind, type, fixing, mark, number).\n     finish: 'auto' follows the nearest data-finish around the section. -->\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>Customers</title>\n<link rel=\"stylesheet\" href=\"./client-plates/client-plates.css\">\n<style>\n  body { margin: 0; }\n</style>\n</head>\n<body>\n<main id=\"customers\"></main>\n<script type=\"module\">\nimport { mount } from './client-plates/client-plates.mjs';\n\nconst block = mount(document.getElementById('customers'), {\n  label: \"Customers\",\n  title: \"Used in production by\",\n  note: \"1,940 teams run agents on Lode, from co-ops to banks.\",\n  count: 6,\n  stats: true,\n  fixing: \"mixed\",\n  motion: \"static\",\n  speed: 40,\n  finish: \"auto\",\n});\n\n// block.update({ motion: 'drift' })   set the plates on the belt\n// block.paused                       true while the Pause key is down\n// block.destroy()                    removes the section and its observers\n</script>\n</body>\n</html>"},{"path":"tactile/client-plates/LICENSE","type":"registry:file","target":"tactile/client-plates/LICENSE","content":"MIT License\n\nCopyright (c) 2026 Ciprian Titire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"}],"docs":"Engraved Client Plates is in tactile/client-plates/. Open example.html to see it run; client-plates.mjs is the class, client-plates.css its look.\nIts fonts (InstrumentSans-Regular.woff2, InstrumentSans-Bold.woff2) are binary, so they come with the piece's download: https://tactileui.dev/api/download/blk.client-plates (put them next to the CSS).\nDocs: https://tactileui.dev/blocks/client-plates","meta":{"id":"blk.client-plates","version":"0.1.0","access":"free","licence":"MIT"}}