/* TitleVerifi — Nodes mark system The chain of title, drawn as a graph. One canonical mark, with animated and dense variations. */ /* ============================================================ ANIMATION KEYFRAMES — one calm traveling pulse along the chain ============================================================ */ if (typeof document !== 'undefined' && !document.getElementById('tv-mark-anim')) { const s = document.createElement('style'); s.id = 'tv-mark-anim'; s.textContent = ` /* A single circle travels the chain path; nodes brighten as it passes. */ @keyframes tv-travel { 0% { offset-distance: 0%; opacity: 0; } 6% { opacity: 1; } 94% { opacity: 1; } 100% { offset-distance: 100%; opacity: 0; } } /* Each node receives a brief brightening at its time-slot in the cycle. */ @keyframes tv-node-active-1 { 0%, 8%, 100% { fill-opacity: 0.45; r: 3.2; } 14%, 22% { fill-opacity: 1; r: 3.8; } } @keyframes tv-node-active-2 { 0%, 32%, 100% { fill-opacity: 0.45; r: 3.2; } 38%, 46% { fill-opacity: 1; r: 3.8; } } @keyframes tv-node-active-3 { 0%, 56%, 100% { fill-opacity: 0.45; r: 3.2; } 62%, 70% { fill-opacity: 1; r: 3.8; } } @keyframes tv-node-active-4 { 0%, 80%, 100% { fill-opacity: 0.45; r: 3.2; } 86%, 94% { fill-opacity: 1; r: 3.8; } } /* Subtle accent pulse — only on the highlighted node when set explicitly. */ @keyframes tv-accent-glow { 0%, 100% { fill-opacity: 1; r: 4; } 50% { fill-opacity: 0.7; r: 4.4; } } /* === SEMANTIC STATES ================================================= Each "state" tells the mark what's happening in the UI. The sequence runs over a configurable cycle length. ==================================================================== */ /* PROCESSING — node "scans": each node briefly inflates with an accent ring and brightens, in sequence. Reads as "examining each record." */ @keyframes tv-proc-scan { 0%, 100% { r: 3.2; fill-opacity: 0.4; } 18%, 28% { r: 4.4; fill-opacity: 1; } } @keyframes tv-proc-ring { 0%, 10% { r: 3.2; opacity: 0; stroke-width: 1.2; } 18% { r: 4.4; opacity: 0.9; } 40% { r: 7; opacity: 0; stroke-width: 0.4; } 100% { r: 7; opacity: 0; } } /* Edges dim, then briefly carry charge along when the upstream node fires. */ @keyframes tv-proc-edge { 0%, 100% { opacity: 0.2; } 15%, 30% { opacity: 0.85; } } /* CONNECTING — hub lands first, edges draw outward from hub, then peripheral nodes materialize at the edge tips. Loops cleanly. */ @keyframes tv-conn-hub { 0% { transform: scale(0); opacity: 0; } 8% { transform: scale(1.25); opacity: 1; } 14%, 100%{ transform: scale(1); opacity: 1; } } @keyframes tv-conn-edge { 0%, 14% { stroke-dashoffset: 24; opacity: 0; } 28%, 88% { stroke-dashoffset: 0; opacity: 0.55; } 96%, 100% { stroke-dashoffset: 0; opacity: 0; } } @keyframes tv-conn-node { 0%, 28% { transform: scale(0); opacity: 0; } 36% { transform: scale(1.35); opacity: 1; } 44%, 88% { transform: scale(1); opacity: 1; } 96%, 100% { transform: scale(1); opacity: 0; } } /* RESOLVING — whole mark rotates 360° once, then settles. */ @keyframes tv-resolve-spin { 0% { transform: rotate(0deg); } 80% { transform: rotate(360deg); } 100% { transform: rotate(360deg); } } /* COMPLETE — brief outward bloom on the accent, all nodes settle lit. */ @keyframes tv-complete-bloom { 0% { r: 3.5; fill-opacity: 0.7; } 40% { r: 5; fill-opacity: 1; } 100% { r: 4; fill-opacity: 1; } } @keyframes tv-complete-ring { 0% { r: 3.5; opacity: 0.6; } 100% { r: 12; opacity: 0; } } /* ERROR — one node shakes laterally, accent flashes. */ @keyframes tv-error-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-1.5px); } 40% { transform: translateX(1.5px); } 60% { transform: translateX(-1px); } 80% { transform: translateX(0.5px); } } @keyframes tv-error-flash { 0%, 100% { fill-opacity: 1; } 30%, 60% { fill-opacity: 0.4; } } /* TV monogram — peripheral nodes brighten as the pulse passes their position on the figure-8 chain. Times are tuned to the pulsePath. */ @keyframes tv-tv-node-0 { 0%, 100% { fill-opacity: 0.55; r: 2.1; } 8%, 14% { fill-opacity: 1; r: 2.5; } } @keyframes tv-tv-node-2 { 0%, 100% { fill-opacity: 0.55; r: 2.1; } 26%, 32% { fill-opacity: 1; r: 2.5; } } @keyframes tv-tv-node-4 { 0%, 100% { fill-opacity: 0.55; r: 2.1; } 44%, 52% { fill-opacity: 1; r: 2.5; } } @keyframes tv-tv-node-3 { 0%, 100% { fill-opacity: 0.55; r: 2.1; } 78%, 86% { fill-opacity: 1; r: 2.5; } } @keyframes cv-shimmer { 0%,100% { fill-opacity: var(--shimmer-base, 0.78); } 50% { fill-opacity: 1; } } @keyframes cv-canonical { 0%,100% { fill-opacity: 1; } 50% { fill-opacity: 0.74; } } `; document.head.appendChild(s); } /* ============================================================ CANONICAL — Nodes (4 nodes), now with semantic UI states. Props: - state: 'idle' | 'processing' | 'connecting' | 'resolving' | 'complete' | 'error' Default 'idle' (calm traveling pulse, the rest state). - activeNode: 0..3 → lock that node lit (overrides animation). Use this for tab/section wayfinding. - cycle: animation cycle length in seconds (default 2.4s for active states, 4s for idle). - animate: master kill-switch (false = static, used by print). ============================================================ */ const MarkNodes = ({ size = 32, color = 'currentColor', accent = 'var(--accent)', animate = true, activeNode = null, state = 'idle', cycle, }) => { // 4 nodes: a chain that reads left → center → right → bottom-right. const nodes = [ { x: 7, y: 9 }, // 0: prior record { x: 16, y: 16 }, // 1: hub (the active title) — accent { x: 25, y: 9 }, // 2: prior record { x: 25, y: 23 }, // 3: derived record ]; const edges = [ { from: 0, to: 1, dur: 0.0 }, // edge 0 { from: 2, to: 1, dur: 0.15 }, // edge 1 { from: 3, to: 1, dur: 0.30 }, // edge 2 ]; // Path the pulse travels along. const pulsePath = "M 7 9 L 16 16 L 25 9 L 16 16 L 25 23 L 16 16 L 7 9"; const isLocked = activeNode !== null && activeNode !== undefined; const isStatic = !animate; // When activeNode is set, force idle behavior + lock — wayfinding wins. const effectiveState = isLocked ? 'locked' : (isStatic ? 'static' : state); const c = cycle || (effectiveState === 'idle' ? 4 : 2.4); // ---- Per-state node renderer ------------------------------------------- const renderNode = (n, i) => { const isHub = i === 1; const baseFill = isHub ? accent : color; // LOCKED — exactly one node lit, others dimmed, no animation. if (effectiveState === 'locked') { const lit = activeNode === i; return ( ); } // STATIC — print/favicon, no animation. if (effectiveState === 'static') { return ; } // IDLE — calm traveling pulse: nodes sequentially brighten. if (effectiveState === 'idle') { return ( ); } // PROCESSING — each node "scans": staggered inflate + accent ring sweep. if (effectiveState === 'processing') { // Scan order: 0 → 2 → 1 (hub) → 3. Hub fires last so it reads as the resolution. const order = [0, 2, 1, 3]; const slot = order.indexOf(i); const delay = slot * (c / 4); return ( ); } // CONNECTING — hub lands first, then peripheral nodes materialize at edge tips. if (effectiveState === 'connecting') { // Hub: lands at t=0. Peripheral nodes: land after their edge draws. // Edges draw 14%→28% with stagger 0/0.15/0.30. Node pops at 36% absolute. const peripheralOrder = { 0: 0, 2: 1, 3: 2 }; // 0,2,3 stagger const stagger = isHub ? 0 : (peripheralOrder[i] || 0) * 0.12; return ( ); } // RESOLVING — handled at group level; just render the nodes lit. if (effectiveState === 'resolving') { return ; } // COMPLETE — all nodes settle lit, accent blooms once + ring expands. if (effectiveState === 'complete') { return ( ); } // ERROR — accent flashes; node 3 shakes (the "wrong" record). if (effectiveState === 'error') { const isShaker = i === 3; return ( ); } return null; }; // ---- Per-state edge renderer ------------------------------------------- const renderEdge = (e, i) => { const a = nodes[e.from], b = nodes[e.to]; if (effectiveState === 'connecting') { // Draw FROM hub OUTWARD to peripheral node. Edge order: 0→1, 2→1, 3→1 // becomes hub(b) → peripheral(a). Stagger: 0 / 0.12 / 0.24. const stagger = i * 0.12; // Use pathLength so dashoffset works regardless of geometry. return ( ); } if (effectiveState === 'processing') { // Edge brightens when its upstream (non-hub) node fires. const order = [0, 2, 1, 3]; const slot = order.indexOf(e.from); const delay = slot * (c / 4); return ( ); } return ( ); }; // ---- Group-level wrapper for resolving (full 360° spin) ---------------- const groupStyle = effectiveState === 'resolving' ? { transformOrigin: '16px 16px', transformBox: 'fill-box', animation: `tv-resolve-spin ${c}s cubic-bezier(.65,.05,.35,1) infinite`, } : undefined; return ( {edges.map(renderEdge)} {nodes.map(renderNode)} {/* Idle traveling pulse */} {effectiveState === 'idle' && ( )} {/* Complete: expanding ring around hub */} {effectiveState === 'complete' && ( )} ); }; /* ============================================================ STATIC — Nodes, no animation ============================================================ */ const MarkNodesStatic = ({ size = 32, color = 'currentColor', accent = 'var(--accent)' }) => ( ); /* ============================================================ Wordmark only ============================================================ */ const MarkNone = () => null; /* ============================================================ TV — node-monogram. The letterforms T and V drawn as a graph. N0 ───── N1 ───── N2 T's bar, with N1 as the shared joint │ ╲ ╲ │ ╲ ╲ │ ╲ ╲ N3 N4 T's stem foot · V's vertex Strokes: N0—N1 T-bar (left half) N1—N2 T-bar (right half) — also the visual "lid" of the V N1—N3 T-stem (vertical) N1—N4 V-left-stroke N2—N4 V-right-stroke N1 is the 4-way hub — it carries the accent. ============================================================ */ const MarkTV = ({ size = 32, color = 'currentColor', accent = 'var(--accent)', animate = true, activeNode = null, state = 'idle', cycle, }) => { const nodes = [ { x: 4, y: 7 }, // 0 T-bar-left tip { x: 13, y: 7 }, // 1 HUB · shared joint (T-stem top + T-bar mid + V-left top) { x: 24, y: 7 }, // 2 T-bar-right tip · V-right top { x: 13, y: 22 }, // 3 T-stem foot { x: 18.5, y: 22 },// 4 V-vertex (bottom point) ]; const edges = [ [0, 1], // T-bar L [1, 2], // T-bar R (also V-lid) [1, 3], // T-stem [1, 4], // V-left [2, 4], // V-right ]; const HUB = 1; const pulsePath = "M 4 7 L 13 7 L 24 7 L 18.5 22 L 13 7 L 13 22 L 13 7"; const isLocked = activeNode !== null && activeNode !== undefined; const isStatic = !animate; const effectiveState = isLocked ? 'locked' : (isStatic ? 'static' : state); const c = cycle || (effectiveState === 'idle' ? 4.5 : 2.4); return ( {/* Edges */} {edges.map(([a, b], i) => ( ))} {/* Nodes */} {nodes.map((n, i) => { const isHub = i === HUB; const fill = isHub ? accent : color; if (effectiveState === 'locked') { const lit = activeNode === i; return ; } if (effectiveState === 'idle') { // Hub stays solid accent; peripherals pulse subtly with the traveling dot. if (isHub) return ; return ; } // static / fallback return ; })} {/* Idle traveling pulse */} {effectiveState === 'idle' && ( )} ); }; /* ============================================================== */ /* CANONICAL MARK · The Chain · Strata (B1) */ /* ============================================================== */ /* Four stacked deed bars, oldest faded at top, canonical at */ /* bottom in vermilion. Vertical tie-spine on the left. */ /* The unbroken chain of title made visible. */ /* */ /* This component intentionally ignores `state`/`activeNode` */ /* props passed by legacy callsites — the LOGO is fixed. */ /* Status indicators live in product UI, not on the mark. */ const MarkStrata = ({ size = 32, color = 'currentColor', accent = 'var(--accent)', animate }) => { const motion = animate === false ? 'static' : 'ambient'; /* Four grey strata (oldest faded) on a 3.5-unit rhythm, then the vermilion canonical bar. Spine ends exactly flush with canonical bottom. */ const bars = [ { y: 6, h: 1.6, op: 0.35 }, { y: 9.5, h: 1.6, op: 0.55 }, { y: 13, h: 1.6, op: 0.75 }, { y: 16.5, h: 1.6, op: 0.92 }, ]; return ( {bars.map((b, i) => ( ))} ); }; const MARKS = { nodes: MarkStrata, // ★ canonical — Chain · Strata (B1) strata: MarkStrata, // explicit alias tv: MarkStrata, // legacy alias → canonical nodesStatic: (props) => , none: MarkNone, }; const Mark = ({ variant = 'nodes', size = 32, color = 'currentColor', accent = 'var(--accent)', animate }) => { const C = MARKS[variant] || MarkStrata; if (variant === 'none') return null; return ; }; const WordmarkStrataMark = ({ size = 18, color = 'currentColor', accent = 'var(--accent)', animate }) => { const motion = animate === false ? 'static' : 'ambient'; const width = size; const height = size / 1.2; const bars = [ { y: 0, h: 1.55, op: 0.35 }, { y: 4.2, h: 1.55, op: 0.55 }, { y: 8.4, h: 1.55, op: 0.75 }, { y: 12.6, h: 1.55, op: 0.92 }, ]; return ( {bars.map((b, i) => ( ))} ); }; const Wordmark = ({ size = 18, color = 'var(--ink)', accent = 'var(--accent)', variant = 'nodes', accentLetter = 'V', animate, activeNode, state, }) => { const showMark = variant !== 'none'; let nameEl; if (accentLetter === 'V') nameEl = <>TitleVerifi; else if (accentLetter === 'i-dot') nameEl = <>TitleVerifi; else nameEl = 'TitleVerifi'; return ( {showMark && } {nameEl} ); }; window.Mark = Mark; window.MARKS = MARKS; window.Wordmark = Wordmark;