Skip to content

Holo Site Footer

The close of a landing: a last call set big with a holo pill between its words and two buttons, then a footer with the mark, a one-line promise, three columns of real links and the copyright with a way back up.

FreeHolo Glossminimalglass
Preview

Ready when you are.

viberdy

Premium React components in every style, built to be pasted.

© 2026 viberdy. All rights reserved.Back to top ↑

Props

No runtime dependencies
"use client";

import { useEffect, useRef, useState, type CSSProperties, type RefObject } from "react";

/* ============================================================================
 * HOLO ART ENGINE
 *
 * Every picture in the kit (the glass cube grid on a card, the violet tubes,
 * the mosaic, the cobalt slabs, the dark tiled room, the concrete steps) is
 * PAINTED IN CODE by one hand-written WebGL2 ray marcher. Nothing is a photo
 * or a video, so an AI agent can retarget a scene by editing its map().
 *
 * How it stays cheap:
 *   - One shared offscreen WebGL2 context for the whole page, created on the
 *     first request and never per component.
 *   - A picture is rendered ONCE per (scene, size, seed) and cached as a
 *     canvas. Cards, pills and tiles draw the cached canvas into their own 2D
 *     canvas; nothing ray-marches per frame.
 *   - Renders are queued and split into bands of rows across animation
 *     frames, so a large scene never blocks one frame for long.
 *   - Without WebGL2 every <HoloArt> keeps its CSS gradient stand-in.
 *
 * Each scene is its own small program: a shared prelude (SDF helpers, a
 * studio environment with crisp rectangular softboxes, thin-film colour,
 * glass with a real interior march, ACES) plus the scene's own map() and
 * camera, compiled the first time that scene is asked for.
 * ========================================================================== */

export type HoloKind = "cubes" | "tubes" | "mosaic" | "slabs" | "rings" | "ribbon" | "foil" | "room" | "steps";

/*GLSL-BEGIN*/
const HG_PRELUDE = `#version 300 es
precision highp float;
uniform vec2 uRes;
uniform vec4 uBand;
uniform float uSeed;
uniform vec2 uFocus;
uniform float uZoom;
out vec4 fragColor;

#define PI 3.14159265
#define MAT_GLASS 1.0
#define MAT_CHROME 2.0
#define MAT_GLOSS 3.0
#define MAT_MATTE 4.0
#define MAT_TILE 5.0
#define MAT_STONE 6.0
#define MAT_SATIN 7.0

mat2 rot(float a){ float c=cos(a), s=sin(a); return mat2(c,-s,s,c); }
float hash(float n){ return fract(sin(n*127.1+uSeed*13.7)*43758.5453); }
float hash2(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))+uSeed*7.1)*43758.5453); }
float vnoise(vec2 p){ vec2 i=floor(p), f=fract(p); f=f*f*(3.0-2.0*f); return mix(mix(hash2(i),hash2(i+vec2(1,0)),f.x),mix(hash2(i+vec2(0,1)),hash2(i+vec2(1,1)),f.x),f.y); }
float sdBox(vec3 p, vec3 b, float r){ vec3 q=abs(p)-b+r; return length(max(q,0.))+min(max(q.x,max(q.y,q.z)),0.)-r; }
float sdSphere(vec3 p, float r){ return length(p)-r; }
float sdTorus(vec3 p, vec2 t){ vec2 q=vec2(length(p.xz)-t.x,p.y); return length(q)-t.y; }
float sdCapsule(vec3 p, vec3 a, vec3 b, float r){ vec3 pa=p-a, ba=b-a; float h=clamp(dot(pa,ba)/dot(ba,ba),0.,1.); return length(pa-ba*h)-r; }
float sdCappedTorus(vec3 p, vec2 sc, float ra, float rb){ p.x=abs(p.x); float k=(sc.y*p.x>sc.x*p.y)?dot(p.xy,sc):length(p.xy); return sqrt(dot(p,p)+ra*ra-2.0*ra*k)-rb; }
float smin(float a, float b, float k){ float h=clamp(0.5+0.5*(b-a)/k,0.,1.); return mix(b,a,h)-k*h*(1.-h); }

// Thin-film interference: the oil-on-water rainbow that makes glass read
// as holographic. cosT is the cosine to the normal, th the film thickness.
vec3 film(float cosT, float th){
  float d = th * (0.35 + 0.65 * cosT);
  return 0.5 + 0.5 * cos(6.28318 * (d * vec3(1.0, 0.86, 0.72) + vec3(0.0, 0.31, 0.63)));
}

// A softbox: bright inside an angular rectangle around axis a, crisp edges.
float softbox(vec3 d, vec3 a, vec3 up, vec2 size, float edge){
  a = normalize(a);
  vec3 rt = normalize(cross(up, a));
  vec3 u2 = cross(a, rt);
  float f = dot(d, a);
  if (f <= 0.0) return 0.0;
  vec2 q = vec2(dot(d, rt), dot(d, u2)) / f;
  vec2 e = smoothstep(size, size - edge, abs(q));
  return e.x * e.y;
}
`;

// The studio the glass objects are photographed in: a pale lavender sweep,
// a big white overhead box, a cobalt panel left, a hot pink panel right and
// an orange bounce low. These are what the glass reflects and refracts, so
// they ARE the kit's palette.
const HG_STUDIO = `
vec3 env(vec3 d){
  float y = d.y * 0.5 + 0.5;
  vec3 c = mix(vec3(0.20, 0.21, 0.30), vec3(0.98, 0.97, 1.0), smoothstep(0.25, 0.85, y));
  c += vec3(2.6) * softbox(d, vec3(0.1, 1.0, 0.35), vec3(0.0, 0.0, -1.0), vec2(0.9, 0.5), 0.12);
  c += vec3(0.10, 0.28, 1.0) * 2.4 * softbox(d, vec3(-1.0, 0.25, 0.25), vec3(0.0, 1.0, 0.0), vec2(0.45, 0.8), 0.18);
  c += vec3(1.0, 0.22, 0.62) * 2.0 * softbox(d, vec3(1.0, 0.15, 0.35), vec3(0.0, 1.0, 0.0), vec2(0.4, 0.7), 0.2);
  c += vec3(1.0, 0.45, 0.10) * 1.3 * softbox(d, vec3(0.3, -1.0, 0.6), vec3(0.0, 0.0, 1.0), vec2(0.7, 0.35), 0.25);
  c += vec3(0.55, 0.40, 1.0) * 0.9 * softbox(d, vec3(-0.2, 0.3, -1.0), vec3(0.0, 1.0, 0.0), vec2(1.2, 0.6), 0.5);
  return c;
}
vec3 backdrop(vec2 uv){
  // What the camera sees behind the objects: near-white with two faint
  // coloured blooms, so art sits on the card's own paper.
  vec3 c = mix(vec3(0.955, 0.955, 0.975), vec3(0.985, 0.982, 0.995), uv.y);
  c += vec3(-0.04, -0.02, 0.05) * exp(-6.0 * dot(uv - vec2(0.15, 0.25), uv - vec2(0.15, 0.25)));
  c += vec3(0.05, -0.02, 0.02) * exp(-6.0 * dot(uv - vec2(0.85, 0.75), uv - vec2(0.85, 0.75)));
  return c;
}
`;

// The ray marcher, shading and tone map, shared by the studio scenes. A scene
// supplies vec4 map(vec3 p) -> (distance, material, colour id, film), vec3
// tint(float id) and the camera through CAM_RO / CAM_TA / CAM_FOV.
const HG_STUDIO_MAIN = `
vec3 nrm(vec3 p){
  vec2 e = vec2(0.0015, 0.0);
  return normalize(vec3(map(p+e.xyy).x-map(p-e.xyy).x, map(p+e.yxy).x-map(p-e.yxy).x, map(p+e.yyx).x-map(p-e.yyx).x));
}
vec4 march(vec3 ro, vec3 rd){
  float t = 0.0;
  for (int i = 0; i < 120; i++){
    vec4 h = map(ro + rd * t);
    if (h.x < 0.0008 * (1.0 + t)) return vec4(t, h.yzw);
    t += h.x * 0.75;
    if (t > 30.0) break;
  }
  return vec4(-1.0);
}
// Thickness of the object along a ray that starts just inside it.
float thick(vec3 p, vec3 d){
  float t = 0.02;
  for (int i = 0; i < 28; i++){
    float h = map(p + d * t).x;
    if (h > -0.0005) break;
    t += max(-h, 0.012);
  }
  return t;
}
float ao(vec3 p, vec3 n){
  float o = 0.0, s = 1.0;
  for (int i = 1; i <= 5; i++){
    float h = 0.03 * float(i);
    o += (h - map(p + n * h).x) * s;
    s *= 0.7;
  }
  return clamp(1.0 - 2.2 * o, 0.0, 1.0);
}
bool gMiss = false;
vec3 shadeStudio(vec3 ro, vec3 rd, vec2 uv){
  vec4 h = march(ro, rd);
  if (h.x < 0.0){ gMiss = true; return backdrop(uv); }
  vec3 p = ro + rd * h.x;
  vec3 n = nrm(p);
  float mat = h.y;
  vec3 col = tint(h.z);
  float cosT = clamp(dot(n, -rd), 0.0, 1.0);
  float fr = 0.04 + 0.96 * pow(1.0 - cosT, 5.0);
  vec3 r = reflect(rd, n);
  vec3 L = normalize(vec3(-0.4, 0.9, 0.6));
  float occ = ao(p, n);
  if (mat == MAT_GLASS){
    vec3 fl = film(cosT, h.w);
    vec3 refl = env(r) * mix(vec3(1.0), fl * 1.6, 0.55);
    vec3 rdIn = refract(rd, n, 1.0 / 1.48);
    float th = thick(p - n * 0.003, rdIn);
    vec3 pOut = p + rdIn * th;
    vec3 nOut = -nrm(pOut);
    vec3 rdOut = refract(rdIn, nOut, 1.48);
    if (dot(rdOut, rdOut) < 0.01) rdOut = reflect(rdIn, nOut);
    vec3 absorb = exp(-(1.0 - col) * th * 7.5);
    vec3 trans = env(rdOut) * absorb * mix(vec3(1.0), col, 0.5);
    // Light scattering inside coloured glass, so a thick block glows.
    trans += col * col * 0.22 * (1.0 - exp(-th * 3.0));
    vec3 c = mix(trans, refl, clamp(fr * 1.25, 0.0, 1.0));
    c += vec3(1.6) * pow(max(dot(r, L), 0.0), 120.0);
    return c * mix(0.8, 1.0, occ);
  }
  if (mat == MAT_CHROME){
    vec3 fl = film(cosT, 1.4 + h.w);
    vec3 c = env(r) * mix(vec3(0.9), fl, 0.25) * mix(col, vec3(1.0), 0.6);
    return c * mix(0.6, 1.0, occ);
  }
  if (mat == MAT_GLOSS){
    float dif = clamp(dot(n, L), 0.0, 1.0) * 0.75 + 0.22;
    vec3 c = col * col * dif * 0.95 * mix(0.5, 1.0, occ);
    c = mix(c, env(r), fr * 0.55) + vec3(1.4) * pow(max(dot(r, L), 0.0), 60.0) * 0.6;
    return c;
  }
  // MAT_MATTE
  float dif = clamp(dot(n, L), 0.0, 1.0) * 0.55 + 0.55;
  return col * dif * mix(0.5, 1.0, occ) + env(r) * fr * 0.25;
}
vec3 aces(vec3 x){ return clamp((x*(2.51*x+0.03))/(x*(2.43*x+0.59)+0.14), 0.0, 1.0); }
void main(){
  vec2 fc = gl_FragCoord.xy + uBand.xy;
  vec2 uv = fc / uRes;
  vec2 q = (fc - 0.5 * uRes) / uRes.y - uFocus;
  vec3 ro = CAM_RO;
  vec3 ta = CAM_TA;
  vec3 ww = normalize(ta - ro);
  vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));
  vec3 vv = cross(uu, ww);
  vec3 rd = normalize(q.x * uu + q.y * vv + CAM_FOV * uZoom * ww);
  vec3 c = shadeStudio(ro, rd, uv);
  if (!gMiss){
    c = aces(c * 1.05);
    c = pow(c, vec3(1.0 / 2.2));
  }
  // A breath of grain so gradients never band on the card.
  c += (hash2(fc) - 0.5) / 255.0;
  fragColor = vec4(c, 1.0);
}
`;

const HG_SCENES: Record<HoloKind, string> = {
  // A 3x3x3 block of rounded glass cubes, coral at the front-left blushing to
  // pink and cobalt at the back, a chrome ball parked on top, a striped plate.
  cubes: `
#define CAM_RO vec3(0.0, 0.25, 4.6)
#define CAM_TA vec3(0.0, 0.0, 0.0)
#define CAM_FOV 2.35
vec3 tint(float id){
  if (id < 0.5) return vec3(1.0, 0.22, 0.32);
  if (id < 1.5) return vec3(1.0, 0.55, 0.78);
  if (id < 2.5) return vec3(0.25, 0.38, 1.0);
  if (id < 3.5) return vec3(0.92, 0.93, 1.0);
  if (id < 4.5) return vec3(1.0, 0.25, 0.25);
  return vec3(0.96);
}
vec4 map(vec3 p){
  vec3 q = p - vec3(-0.15, -0.05, 0.0);
  q.xz *= rot(0.72);
  q.yz *= rot(-0.5);
  vec3 c = clamp(floor(q / 0.36 + 0.5), -1.0, 1.0);
  vec3 r = q - c * 0.36;
  float d = sdBox(r, vec3(0.152), 0.035);
  float k = c.x - c.z * 0.8 + c.y * 0.35 + 1.5;
  float id = k < 0.9 ? 0.0 : (k < 2.2 ? 1.0 : 2.0);
  vec4 res = vec4(d, MAT_GLASS, id, 1.1 + 0.4 * c.y);
  float s = sdSphere(p - vec3(0.78, 0.72, 0.25), 0.27);
  if (s < res.x) res = vec4(s, MAT_CHROME, 3.0, 0.2);
  vec3 pp = p - vec3(-0.55, -0.92, 0.2);
  pp.xz *= rot(0.5);
  float pl = sdBox(pp, vec3(0.75, 0.02, 0.45), 0.015);
  if (pl < res.x){
    float stripe = step(0.5, fract((pp.x + pp.z) * 9.0));
    res = vec4(pl, MAT_GLOSS, stripe > 0.5 ? 4.0 : 5.0, 0.0);
  }
  return res;
}
`,
  // Two fat iridescent arcs crossing, a glass ball and a chrome bead.
  tubes: `
#define CAM_RO vec3(0.0, 0.2, 4.4)
#define CAM_TA vec3(0.0, 0.0, 0.0)
#define CAM_FOV 2.2
vec3 tint(float id){
  if (id < 0.5) return vec3(0.52, 0.36, 1.0);
  if (id < 1.5) return vec3(0.22, 0.42, 1.0);
  if (id < 2.5) return vec3(1.0, 0.62, 0.86);
  return vec3(0.95);
}
vec4 map(vec3 p){
  vec3 a = p - vec3(-0.25, -0.1, 0.0);
  a.xy *= rot(0.6); a.yz *= rot(1.1);
  float t1 = sdTorus(a, vec2(0.75, 0.24));
  vec3 b = p - vec3(0.45, 0.25, -0.35);
  b.xy *= rot(-0.9); b.xz *= rot(0.7);
  float t2 = sdCappedTorus(b, vec2(sin(2.2), cos(2.2)), 0.62, 0.2);
  vec4 res = vec4(t1, MAT_GLASS, 0.0, 1.8);
  if (t2 < res.x) res = vec4(t2, MAT_GLASS, 1.0, 2.3);
  float g = sdSphere(p - vec3(0.15, -0.55, 0.65), 0.34);
  if (g < res.x) res = vec4(g, MAT_GLASS, 2.0, 1.2);
  float c = sdSphere(p - vec3(-0.95, 0.62, 0.3), 0.17);
  if (c < res.x) res = vec4(c, MAT_CHROME, 3.0, 0.4);
  return res;
}
`,
  // A slab of glossy tiles of different heights: orange, red, cobalt, ice.
  mosaic: `
#define CAM_RO vec3(0.5, 3.1, 2.3)
#define CAM_TA vec3(0.0, -0.2, 0.0)
#define CAM_FOV 2.0
vec3 tint(float id){
  if (id < 0.5) return vec3(1.0, 0.45, 0.05);
  if (id < 1.5) return vec3(1.0, 0.12, 0.10);
  if (id < 2.5) return vec3(0.10, 0.26, 1.0);
  if (id < 3.5) return vec3(0.45, 0.65, 1.0);
  if (id < 4.5) return vec3(0.97, 0.96, 1.0);
  return vec3(0.18, 0.42, 1.0);
}
vec4 map(vec3 p){
  vec3 q = p;
  q.xz *= rot(0.35);
  vec2 base = floor(q.xz / 0.42 + 0.5);
  vec4 res = vec4(1e5);
  for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++){
    vec2 cell = clamp(base + vec2(float(i), float(j)), vec2(-4.0, -3.0), vec2(4.0, 3.0));
    vec2 r = q.xz - cell * 0.42;
    float hh = hash2(cell + 3.0);
    float h = 0.04 + 0.16 * hh * hh;
    float d = sdBox(vec3(r.x, q.y + 0.4 - h, r.y), vec3(0.195, h, 0.195), 0.03);
    if (d < res.x) res = vec4(d, MAT_GLOSS, floor(hash2(cell * 1.7 + 9.0) * 6.0), 1.4);
  }
  float s = sdSphere(p - vec3(0.55, 0.35, 0.55), 0.24);
  if (s < res.x) res = vec4(s, MAT_GLASS, 4.0, 1.6);
  return res;
}
`,
  // Five thin cobalt glass panes fanned along a diagonal.
  slabs: `
#define CAM_RO vec3(0.0, 0.4, 4.6)
#define CAM_TA vec3(0.0, 0.0, 0.0)
#define CAM_FOV 2.2
vec3 tint(float id){
  if (id < 0.5) return vec3(0.02, 0.12, 0.95);
  if (id < 1.5) return vec3(0.10, 0.30, 1.0);
  return vec3(0.45, 0.62, 1.0);
}
vec4 map(vec3 p){
  vec4 res = vec4(1e5);
  for (int i = 0; i < 5; i++){
    float f = float(i) - 2.0;
    vec3 q = p - vec3(f * 0.28, f * 0.2, -f * 0.32);
    q.xy *= rot(0.55); q.yz *= rot(-0.7); q.xz *= rot(0.25);
    float d = sdBox(q, vec3(0.62, 0.42, 0.065), 0.04);
    if (d < res.x) res = vec4(d, MAT_GLASS, i == 0 ? 2.0 : (i < 3 ? 1.0 : 0.0), 0.9 + 0.2 * f);
  }
  return res;
}
`,
  // Three concentric rings: cobalt, porcelain and coral, seen off-axis.
  rings: `
#define CAM_RO vec3(0.0, 0.3, 4.2)
#define CAM_TA vec3(0.0, 0.0, 0.0)
#define CAM_FOV 2.0
vec3 tint(float id){
  if (id < 0.5) return vec3(0.12, 0.28, 1.0);
  if (id < 1.5) return vec3(0.96, 0.96, 1.0);
  if (id < 2.5) return vec3(1.0, 0.28, 0.3);
  return vec3(0.5, 0.65, 1.0);
}
vec4 map(vec3 p){
  vec3 q = p - vec3(0.2, 0.0, 0.0);
  q.yz *= rot(1.15); q.xy *= rot(0.4);
  vec4 res = vec4(1e5);
  for (int i = 0; i < 3; i++){
    float R = 0.32 + 0.33 * float(i);
    float d = sdTorus(q - vec3(0.0, 0.05 * float(i), 0.0), vec2(R, 0.12));
    float m = i == 1 ? MAT_GLOSS : MAT_GLASS;
    if (d < res.x) res = vec4(d, m, float(i), 1.3 + 0.3 * float(i));
  }
  float s = sdSphere(p - vec3(-1.0, 0.45, 0.4), 0.2);
  if (s < res.x) res = vec4(s, MAT_GLASS, 3.0, 1.6);
  return res;
}
`,
  // A long twisted band of foil glass for the capsule pills.
  ribbon: `
#define CAM_RO vec3(0.0, 0.0, 4.2)
#define CAM_TA vec3(0.0, 0.0, 0.0)
#define CAM_FOV 2.4
vec3 tint(float id){
  if (id < 0.5) return vec3(0.62, 0.42, 1.0);
  if (id < 1.5) return vec3(1.0, 0.58, 0.86);
  return vec3(0.25, 0.45, 1.0);
}
vec4 map(vec3 p){
  vec3 q = p;
  q.xy *= rot(-0.12);
  float tw = q.x * 1.35 + uSeed * 0.7;
  q.yz *= rot(tw);
  float d = sdBox(q, vec3(2.6, 0.32, 0.05), 0.04) * 0.6;
  float id = fract(p.x * 0.18 + 0.5) < 0.5 ? 0.0 : 1.0;
  vec4 res = vec4(d, MAT_GLASS, id, 1.4 + 0.6 * sin(p.x * 0.9));
  vec3 b = p - vec3(0.5, 0.05, -0.55);
  b.yz *= rot(1.2);
  float t = sdTorus(b, vec2(0.55, 0.16));
  if (t < res.x) res = vec4(t, MAT_GLASS, 2.0, 2.0);
  float s = sdSphere(p - vec3(-0.9, 0.1, 0.45), 0.2);
  if (s < res.x) res = vec4(s, MAT_CHROME, 1.0, 0.6);
  return res;
}
`,
  // A close-up of a fat twisted torus of foil glass: fills any frame with
  // colour, for pills and ribbons. The seed turns the torus.
  foil: `
#define CAM_RO vec3(0.0, 0.0, 2.6)
#define CAM_TA vec3(0.0, 0.0, 0.0)
#define CAM_FOV 1.5
vec3 tint(float id){
  if (id < 0.5) return vec3(0.55, 0.36, 1.0);
  if (id < 1.5) return vec3(1.0, 0.42, 0.74);
  if (id < 2.5) return vec3(0.16, 0.32, 1.0);
  return vec3(1.0, 0.52, 0.16);
}
vec4 map(vec3 p){
  // Wide frames (pills, ribbons) stretch the glass sideways to fill them.
  p.x /= max(1.0, (uRes.x / uRes.y) / 1.7);
  vec3 q = p;
  q.xy *= rot(0.35 + uSeed * 0.9);
  q.yz *= rot(1.25 + uSeed * 0.37);
  float a = atan(q.z, q.x);
  vec3 r = vec3(length(q.xz) - 1.35, q.y, 0.0);
  r.xy *= rot(a * 1.5 + uSeed);
  float d = (length(vec2(r.x * 0.62, r.y * 1.25)) - 0.5) * 0.7;
  float id = mod(floor((a + 3.1416) / 1.0471 + uSeed), 3.0);
  vec4 res = vec4(d, MAT_GLASS, id, 1.5 + 0.8 * sin(a * 2.0));
  float s = sdSphere(p - vec3(0.95, -0.55, 0.5), 0.28);
  if (s < res.x) res = vec4(s, MAT_CHROME, 1.0, 0.6);
  float t2 = sdTorus((p - vec3(-1.1, 0.7, -0.6)).xzy, vec2(0.55, 0.16));
  if (t2 < res.x) res = vec4(t2, MAT_GLASS, 3.0, 1.2);
  return res;
}
`,
  room: "",
  steps: "",
};

// The two dark scenes have their own lighting: a charcoal tiled wall, a
// stone floor and satin metal forks (room); a concrete ledge seen from above
// (steps). Same marcher idea, one key light, soft shadows, no studio colour.
const HG_DARK = `
vec4 map(vec3 p);
vec3 nrm(vec3 p){
  vec2 e = vec2(0.002, 0.0);
  return normalize(vec3(map(p+e.xyy).x-map(p-e.xyy).x, map(p+e.yxy).x-map(p-e.yxy).x, map(p+e.yyx).x-map(p-e.yyx).x));
}
vec4 march(vec3 ro, vec3 rd){
  float t = 0.0;
  for (int i = 0; i < 160; i++){
    vec4 h = map(ro + rd * t);
    if (h.x < 0.0006 * (1.0 + t)) return vec4(t, h.yzw);
    t += h.x * 0.85;
    if (t > 40.0) break;
  }
  return vec4(-1.0);
}
float shadow(vec3 ro, vec3 rd){
  float r = 1.0, t = 0.03;
  for (int i = 0; i < 48; i++){
    float h = map(ro + rd * t).x;
    r = min(r, 12.0 * h / t);
    t += clamp(h, 0.02, 0.4);
    if (r < 0.002 || t > 12.0) break;
  }
  return clamp(r, 0.0, 1.0);
}
float ao(vec3 p, vec3 n){
  float o = 0.0, s = 1.0;
  for (int i = 1; i <= 5; i++){
    float h = 0.04 * float(i);
    o += (h - map(p + n * h).x) * s;
    s *= 0.75;
  }
  return clamp(1.0 - 2.0 * o, 0.0, 1.0);
}
vec3 aces(vec3 x){ return clamp((x*(2.51*x+0.03))/(x*(2.43*x+0.59)+0.14), 0.0, 1.0); }
`;

const HG_ROOM = `
#define CAM_RO vec3(0.0, 1.55, 7.6)
#define CAM_TA vec3(0.0, 1.15, 0.0)
#define CAM_FOV 2.05
float tileWall(vec3 p){
  vec2 c = floor(p.xy / 1.02);
  vec2 r = p.xy - (c + 0.5) * 1.02;
  float bulge = 0.02 * hash2(c);
  return sdBox(vec3(r, p.z + 1.6 - bulge), vec3(0.47, 0.47, 0.12), 0.09);
}
vec4 map(vec3 p){
  vec4 res = vec4(tileWall(p), MAT_TILE, 0.0, 0.0);
  float back = p.z + 1.62;
  if (back < res.x) res = vec4(back, MAT_TILE, 1.0, 0.0);
  // Stone floor: blocks with grooves, a raised step to the right.
  vec2 fc = floor(vec2(p.x / 1.6, p.z / 0.9));
  vec2 fr = vec2(p.x, p.z) - (fc + 0.5) * vec2(1.6, 0.9);
  float fl = sdBox(vec3(fr.x, p.y + 0.3, fr.y), vec3(0.78, 0.3, 0.43), 0.02);
  if (fl < res.x) res = vec4(fl, MAT_STONE, hash2(fc), 0.0);
  float step1 = sdBox(p - vec3(4.6, 0.35, -0.6), vec3(1.4, 0.35, 0.9), 0.02);
  if (step1 < res.x) res = vec4(step1, MAT_STONE, 0.6, 0.0);
  float plinth = sdBox(p - vec3(-4.9, 0.18, -0.3), vec3(1.1, 0.18, 1.2), 0.02);
  if (plinth < res.x) res = vec4(plinth, MAT_STONE, 0.3, 0.0);
  return res;
}
void main(){
  vec2 fc = gl_FragCoord.xy + uBand.xy;
  vec2 q = (fc - 0.5 * uRes) / uRes.y - uFocus;
  vec3 ro = CAM_RO, ta = CAM_TA;
  vec3 ww = normalize(ta - ro), uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0))), vv = cross(uu, ww);
  vec3 rd = normalize(q.x * uu + q.y * vv + CAM_FOV * uZoom * ww);
  vec3 col = vec3(0.02);
  vec4 h = march(ro, rd);
  if (h.x > 0.0){
    vec3 p = ro + rd * h.x;
    vec3 n = nrm(p);
    vec3 L = normalize(vec3(-0.35, 0.85, 0.55));
    float sh = shadow(p + n * 0.01, L);
    float oc = ao(p, n);
    float dif = clamp(dot(n, L), 0.0, 1.0);
    vec3 r = reflect(rd, n);
    float fr = 0.04 + 0.96 * pow(1.0 - clamp(dot(n, -rd), 0.0, 1.0), 5.0);
    // A tall soft window left of camera, what glossy surfaces mirror.
    float win = smoothstep(0.2, 0.9, r.y) * smoothstep(-0.9, 0.2, -r.x);
    if (h.y == MAT_TILE){
      vec3 base = vec3(0.045, 0.046, 0.052);
      col = base * (0.2 + 1.1 * dif * sh) * oc + vec3(0.45) * win * fr * 0.9 * oc;
      col += vec3(0.9) * pow(max(dot(r, L), 0.0), 40.0) * 0.35 * sh;
    } else if (h.y == MAT_STONE){
      float grain = vnoise(p.xz * 30.0) * 0.03;
      vec3 base = vec3(0.16, 0.16, 0.165) * (0.8 + 0.25 * h.z) + grain;
      col = base * (0.2 + 1.1 * dif * sh) * oc + vec3(0.25) * win * fr * 0.4;
    } else {
      // Satin metal: a pale grey that holds the window as a broad sheen.
      vec3 base = vec3(0.52, 0.53, 0.56);
      float spec = pow(max(dot(r, L), 0.0), 18.0);
      col = base * (0.18 + 0.9 * dif * sh) * oc + vec3(0.9) * win * 0.55 + vec3(1.2) * spec * sh * 0.5;
      col += vec3(0.4) * smoothstep(0.0, 1.0, r.y) * 0.3;
    }
    float fog = exp(-0.012 * h.x * h.x);
    col = mix(vec3(0.03, 0.03, 0.035), col, fog);
  }
  // A soft overhead pool of light, darker corners.
  vec2 uv = fc / uRes;
  col *= 0.75 + 0.5 * exp(-3.0 * dot(uv - vec2(0.5, 0.75), uv - vec2(0.5, 0.75)));
  col = aces(col * 1.5);
  col = pow(col, vec3(1.0 / 2.2));
  col += (hash2(fc) - 0.5) / 255.0;
  fragColor = vec4(col, 1.0);
}
`;

const HG_STEPS = `
#define CAM_RO vec3(-2.2, 6.2, 4.4)
#define CAM_TA vec3(0.6, 0.0, 0.0)
#define CAM_FOV 2.0
vec4 map(vec3 p){
  // A concrete ledge running diagonally, a lower floor, a tiled wall behind.
  vec3 q = p;
  q.xz *= rot(0.62);
  float ledge = sdBox(q - vec3(0.0, 0.5, -0.6), vec3(9.0, 0.5, 1.5), 0.03);
  vec2 lc = floor(vec2(q.x / 1.5, 0.0));
  vec4 res = vec4(ledge, MAT_STONE, hash2(lc), 0.0);
  float floor0 = p.y;
  if (floor0 < res.x) res = vec4(floor0, MAT_STONE, 0.4, 1.0);
  vec3 w = q - vec3(0.0, 0.0, -3.2);
  vec2 c = floor(w.xy / 0.9);
  vec2 r = w.xy - (c + 0.5) * 0.9;
  float tiles = sdBox(vec3(r, w.z), vec3(0.42, 0.42, 1.1), 0.08);
  if (tiles < res.x) res = vec4(tiles, MAT_TILE, 0.0, 0.0);
  return res;
}
void main(){
  vec2 fc = gl_FragCoord.xy + uBand.xy;
  vec2 q = (fc - 0.5 * uRes) / uRes.y - uFocus;
  vec3 ro = CAM_RO, ta = CAM_TA;
  vec3 ww = normalize(ta - ro), uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0))), vv = cross(uu, ww);
  vec3 rd = normalize(q.x * uu + q.y * vv + CAM_FOV * uZoom * ww);
  vec3 col = vec3(0.05);
  vec4 h = march(ro, rd);
  if (h.x > 0.0){
    vec3 p = ro + rd * h.x;
    vec3 n = nrm(p);
    vec3 L = normalize(vec3(0.5, 0.8, 0.35));
    float sh = shadow(p + n * 0.01, L);
    float oc = ao(p, n);
    float dif = clamp(dot(n, L), 0.0, 1.0);
    vec3 qq = p; qq.xz *= rot(0.62);
    // Board-formed concrete: faint planks, pores, edge wear.
    float plank = 0.018 * sin(qq.x * 7.0 + vnoise(qq.xz * 3.0) * 3.0);
    float pores = (vnoise(p.xz * 40.0) * 0.6 + vnoise(p.xz * 9.0) * 0.4) * 0.06 - smoothstep(0.82, 0.9, vnoise(p.xz * 70.0)) * 0.05;
    vec3 base = h.y == MAT_TILE ? vec3(0.03, 0.032, 0.036) : vec3(0.3, 0.3, 0.3) * (0.85 + 0.2 * h.z) + plank + pores - 0.025;
    if (h.w > 0.5) base *= 0.55;
    col = base * (0.28 + 1.05 * dif * sh) * oc;
    col += vec3(0.08) * pow(1.0 - clamp(dot(n, -rd), 0.0, 1.0), 4.0);
  }
  col = aces(col * 0.95);
  col = pow(col, vec3(1.0 / 2.2));
  col += (hash2(fc) - 0.5) / 255.0;
  fragColor = vec4(col, 1.0);
}
`;

/*GLSL-END*/

const HG_VERT = `#version 300 es
in vec2 aPos;
void main(){ gl_Position = vec4(aPos, 0.0, 1.0); }
`;

function hgFragment(kind: HoloKind): string {
  if (kind === "room") return HG_PRELUDE + HG_DARK + HG_ROOM;
  if (kind === "steps") return HG_PRELUDE + HG_DARK + HG_STEPS;
  return HG_PRELUDE + HG_STUDIO + "\n" + HG_SCENES[kind] + HG_STUDIO_MAIN;
}

/** CSS stand-ins shown until (or instead of) the rendered picture. */
export const HOLO_FALLBACK: Record<HoloKind, string> = {
  cubes: "radial-gradient(60% 70% at 35% 45%, #ff8fb8 0%, #ff4d6d 30%, #6f7dff 62%, rgba(240,241,248,0) 80%), #f2f2f8",
  tubes: "radial-gradient(60% 70% at 45% 50%, #b49bff 0%, #6a5cff 35%, #ff9ad5 60%, rgba(240,241,248,0) 82%), #f2f2f8",
  mosaic: "linear-gradient(120deg, #ff7a1a 0 22%, #2f5bff 22% 46%, #ff3b30 46% 64%, #a8c6ff 64% 82%, #f4f4fa 82%)",
  slabs: "radial-gradient(70% 70% at 50% 50%, #3f63ff 0%, #2546ff 35%, #a9bcff 70%, #eef1fb 90%)",
  rings: "radial-gradient(circle at 55% 50%, #f4f4fa 0 18%, #2a48ff 19% 34%, #f4f4fa 35% 46%, #ff4b4f 47% 56%, #c9d4ff 57% 70%, #f2f2f8 71%)",
  ribbon: "linear-gradient(100deg, #8a63ff 0%, #ff8fd0 30%, #f6f2ff 45%, #6f7dff 62%, #ff9ad5 80%, #7b5cff 100%)",
  foil: "linear-gradient(100deg, #6a4dff 0%, #ff6fb5 28%, #ffd2ec 42%, #3a5cff 60%, #b06bff 80%, #ff8a3d 100%)",
  room: "linear-gradient(180deg, #121316 0%, #1b1c20 70%, #2a2b2f 100%)",
  steps: "linear-gradient(160deg, #0d0e10 0%, #3a3b3d 55%, #6b6b6c 100%)",
};

interface HgJob {
  kind: HoloKind;
  w: number;
  h: number;
  seed: number;
  focus: [number, number, number];
  resolve: (c: HTMLCanvasElement | null) => void;
}

interface HgEngine {
  gl: WebGL2RenderingContext;
  canvas: HTMLCanvasElement;
  buf: WebGLBuffer;
  programs: Map<HoloKind, WebGLProgram | null>;
  queue: HgJob[];
  busy: boolean;
  cache: Map<string, Promise<HTMLCanvasElement | null>>;
  lost: boolean;
}

const HG_KEY = Symbol.for("viberdy.holo-gloss.engine.v1");

function hgEngine(): HgEngine | null {
  if (typeof window === "undefined") return null;
  const g = globalThis as unknown as Record<symbol, HgEngine | null | undefined>;
  if (g[HG_KEY] !== undefined) return g[HG_KEY] ?? null;
  const canvas = document.createElement("canvas");
  const gl = canvas.getContext("webgl2", { antialias: false, alpha: false, preserveDrawingBuffer: true, premultipliedAlpha: false });
  if (!gl) {
    g[HG_KEY] = null;
    return null;
  }
  const buf = gl.createBuffer();
  if (!buf) {
    g[HG_KEY] = null;
    return null;
  }
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
  const engine: HgEngine = { gl, canvas, buf, programs: new Map(), queue: [], busy: false, cache: new Map(), lost: false };
  canvas.addEventListener("webglcontextlost", (e) => {
    e.preventDefault();
    engine.lost = true;
    engine.cache.clear();
  });
  g[HG_KEY] = engine;
  return engine;
}

function hgProgram(e: HgEngine, kind: HoloKind): WebGLProgram | null {
  if (e.programs.has(kind)) return e.programs.get(kind) ?? null;
  const gl = e.gl;
  const make = (type: number, src: string) => {
    const s = gl.createShader(type);
    if (!s) return null;
    gl.shaderSource(s, src);
    gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
      console.warn("[holo-art] " + kind + ": " + gl.getShaderInfoLog(s));
      gl.deleteShader(s);
      return null;
    }
    return s;
  };
  const vs = make(gl.VERTEX_SHADER, HG_VERT);
  const fs = make(gl.FRAGMENT_SHADER, hgFragment(kind));
  let prog: WebGLProgram | null = null;
  if (vs && fs) {
    prog = gl.createProgram();
    if (prog) {
      gl.attachShader(prog, vs);
      gl.attachShader(prog, fs);
      gl.bindAttribLocation(prog, 0, "aPos");
      gl.linkProgram(prog);
      if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
        console.warn("[holo-art] link " + kind + ": " + gl.getProgramInfoLog(prog));
        prog = null;
      }
    }
  }
  e.programs.set(kind, prog);
  return prog;
}

function hgPump(e: HgEngine) {
  if (e.busy) return;
  const job = e.queue.shift();
  if (!job) return;
  e.busy = true;
  const done = (c: HTMLCanvasElement | null) => {
    job.resolve(c);
    e.busy = false;
    requestAnimationFrame(() => hgPump(e));
  };
  if (e.lost || e.gl.isContextLost()) return done(null);
  const prog = hgProgram(e, job.kind);
  if (!prog) return done(null);
  const gl = e.gl;
  e.canvas.width = job.w;
  e.canvas.height = job.h;
  gl.viewport(0, 0, job.w, job.h);
  gl.useProgram(prog);
  gl.bindBuffer(gl.ARRAY_BUFFER, e.buf);
  gl.enableVertexAttribArray(0);
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
  gl.uniform2f(gl.getUniformLocation(prog, "uRes"), job.w, job.h);
  gl.uniform1f(gl.getUniformLocation(prog, "uSeed"), job.seed);
  gl.uniform2f(gl.getUniformLocation(prog, "uFocus"), job.focus[0], job.focus[1]);
  gl.uniform1f(gl.getUniformLocation(prog, "uZoom"), job.focus[2]);
  const uBand = gl.getUniformLocation(prog, "uBand");
  // Bands of rows, one band per frame, sized so a band is ~120k pixels.
  const rows = Math.max(16, Math.floor(120000 / job.w));
  let y = 0;
  gl.enable(gl.SCISSOR_TEST);
  const step = () => {
    if (e.lost || gl.isContextLost()) return done(null);
    const hgt = Math.min(rows, job.h - y);
    gl.scissor(0, y, job.w, hgt);
    gl.uniform4f(uBand, 0, 0, 0, 0);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
    y += hgt;
    if (y < job.h) {
      requestAnimationFrame(step);
      return;
    }
    gl.disable(gl.SCISSOR_TEST);
    const out = document.createElement("canvas");
    out.width = job.w;
    out.height = job.h;
    const ctx = out.getContext("2d");
    if (!ctx) return done(null);
    ctx.drawImage(e.canvas, 0, 0);
    done(out);
  };
  step();
}

/**
 * Render (or fetch from cache) one picture. Sizes are bucketed to 32px so a
 * card resizing by a few pixels reuses its picture.
 */
export function requestHoloArt(
  kind: HoloKind,
  w: number,
  h: number,
  seed = 0,
  focus: [number, number] | [number, number, number] = [0, 0],
): Promise<HTMLCanvasElement | null> {
  const f: [number, number, number] = [focus[0], focus[1], focus[2] ?? 1];
  const e = hgEngine();
  if (!e) return Promise.resolve(null);
  const bw = Math.max(32, Math.min(2400, Math.ceil(w / 32) * 32));
  const bh = Math.max(32, Math.min(2400, Math.ceil(h / 32) * 32));
  const key = kind + ":" + bw + "x" + bh + ":" + seed + ":" + f.join(",");
  const hit = e.cache.get(key);
  if (hit) return hit;
  const p = new Promise<HTMLCanvasElement | null>((resolve) => {
    e.queue.push({ kind, w: bw, h: bh, seed, focus: f, resolve });
    hgPump(e);
  });
  e.cache.set(key, p);
  return p;
}

/* ============================================================================
 * SHARED PIECES
 * ========================================================================== */

/** The kit's face. Urbanist is set at 300 for the quiet line, 700 for the loud one. */
export const HG_FONT = 'var(--font-urbanist, "Urbanist"), "Outfit", ui-sans-serif, system-ui, sans-serif';

/** The kit's palette. Ground and ink do the work; the colour lives inside the glass. */
export const HG = {
  ground: "#eef0f5",
  paper: "#f7f8fb",
  ink: "#15161b",
  inkSoft: "rgba(21,22,27,0.62)",
  inkMute: "rgba(21,22,27,0.42)",
  line: "rgba(21,22,27,0.10)",
  night: "#0d0e11",
  cobalt: "#2b46ff",
  violet: "#7b5cff",
  pink: "#ff6fb5",
  coral: "#ff4a4a",
  orange: "#ff8a1f",
} as const;

/** True once the element has come within `margin` of the viewport. Never goes back. */
export function useHgSeen<T extends HTMLElement>(ref: RefObject<T | null>, margin = "200px"): boolean {
  const [seen, setSeen] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el || seen) return;
    if (typeof IntersectionObserver === "undefined") {
      // No observer to wait for: count it as seen on the next frame.
      const id = requestAnimationFrame(() => setSeen(true));
      return () => cancelAnimationFrame(id);
    }
    // Watch against the box that scrolls the element when there is one: the
    // margin only reaches ahead inside its own root, so against the window
    // it would do nothing for a page living in a scrolling preview.
    const io = new IntersectionObserver(
      (es) => {
        if (es.some((e) => e.isIntersecting)) {
          setSeen(true);
          io.disconnect();
        }
      },
      { root: hgScrollParent(el), rootMargin: margin },
    );
    io.observe(el);
    return () => io.disconnect();
  }, [ref, margin, seen]);
  return seen;
}

/**
 * The scrolling box an element lives in: the nearest ancestor that actually
 * scrolls, or null for the window. Resolved lazily on every scroll, because a
 * scroller only becomes one once its content is taller than it.
 */
export function hgScrollParent(el: Element | null): HTMLElement | null {
  let n = el?.parentElement ?? null;
  while (n && n !== document.body && n !== document.documentElement) {
    const oy = getComputedStyle(n).overflowY;
    if ((oy === "auto" || oy === "scroll" || oy === "overlay") && n.scrollHeight > n.clientHeight + 1) return n;
    n = n.parentElement;
  }
  return null;
}

/**
 * A picture from the art engine, filling its box. Shows the scene's CSS
 * stand-in until the render lands (or forever, without WebGL2), then fades
 * the render in. Renders only once the box is near the viewport.
 */
export function HoloArt({
  kind,
  seed = 0,
  focus,
  className,
  style,
  quality = 1,
}: {
  kind: HoloKind;
  seed?: number;
  /** Shift the subject inside the frame, in frame heights, and zoom: [x, y, zoom = 1]. */
  focus?: [number, number] | [number, number, number];
  className?: string;
  style?: CSSProperties;
  /** Render resolution as a fraction of device pixels (capped at 2x). */
  quality?: number;
}) {
  const box = useRef<HTMLDivElement>(null);
  const canvas = useRef<HTMLCanvasElement>(null);
  const seen = useHgSeen(box, "600px");
  const [size, setSize] = useState<[number, number] | null>(null);
  const [ready, setReady] = useState(false);
  const fx = focus?.[0] ?? 0;
  const fy = focus?.[1] ?? 0;
  const fz = focus?.[2] ?? 1;
  useEffect(() => {
    const el = box.current;
    if (!el) return;
    const measure = () => {
      const r = el.getBoundingClientRect();
      // offsetWidth ignores transforms, so a card scaled in 3D still asks
      // for its true layout size.
      const w = el.offsetWidth || r.width;
      const h = el.offsetHeight || r.height;
      if (w > 4 && h > 4) setSize([w, h]);
    };
    measure();
    // A box that is animating its size (a pill opening) would otherwise ask
    // for a render at every step; wait until it has held still.
    let t = 0;
    const settle = () => {
      window.clearTimeout(t);
      t = window.setTimeout(measure, 220);
    };
    const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(settle) : null;
    ro?.observe(el);
    return () => {
      ro?.disconnect();
      window.clearTimeout(t);
    };
  }, []);
  useEffect(() => {
    if (!seen || !size) return;
    let alive = true;
    const dpr = Math.min(2, window.devicePixelRatio || 1) * quality;
    requestHoloArt(kind, size[0] * dpr, size[1] * dpr, seed, [fx, fy, fz]).then((art) => {
      const c = canvas.current;
      if (!alive || !art || !c) return;
      c.width = art.width;
      c.height = art.height;
      c.getContext("2d")?.drawImage(art, 0, 0);
      setReady(true);
    });
    return () => {
      alive = false;
    };
  }, [seen, size, kind, seed, fx, fy, fz, quality]);
  // A box placed by its class keeps that placement; otherwise it becomes the
  // canvas's containing block.
  const placed = (className ?? "").split(" ").some((c) => c === "absolute" || c === "fixed" || c === "sticky");
  return (
    <div
      ref={box}
      aria-hidden
      className={className}
      style={{ position: placed ? undefined : "relative", overflow: "hidden", background: HOLO_FALLBACK[kind], ...style }}
    >
      <canvas
        ref={canvas}
        style={{
          position: "absolute",
          inset: 0,
          width: "100%",
          height: "100%",
          opacity: ready ? 1 : 0,
          transition: "opacity 700ms ease",
        }}
      />
    </div>
  );
}

/** The eight-spoke asterisk the kit uses as caret, bullet and spinner. */
export function HgAsterisk({ className, style, stroke = 1.6 }: { className?: string; style?: CSSProperties; stroke?: number }) {
  return (
    <svg viewBox="0 0 24 24" aria-hidden className={className} style={style} fill="none" stroke="currentColor" strokeWidth={stroke} strokeLinecap="round">
      <path d="M12 3v18M3 12h18M5.6 5.6l12.8 12.8M18.4 5.6 5.6 18.4" />
    </svg>
  );
}

/** viberdy's V, as a solid mark. */
export function HgVMark({ className, style }: { className?: string; style?: CSSProperties }) {
  return (
    <svg viewBox="0 0 32 32" aria-hidden className={className} style={style}>
      <rect width="32" height="32" rx="9" fill="currentColor" />
      <path d="M9 10.5 L15.2 22.5 L16.8 22.5 L23 10.5 L19.6 10.5 L16 18.2 L12.4 10.5 Z" fill="#fff" />
    </svg>
  );
}

/* ============================================================================
 * PLANS: three glass cards dealt out of a pile
 * ========================================================================== */

/** Allow in-page anchors, site paths and http(s)/mailto links; anything else becomes "#". */
export function hgSafeHref(href: string): string {
  const h = href.trim();
  if (h.includes("\\")) return "#";
  if (h.startsWith("#") || (h.startsWith("/") && !h.startsWith("//"))) return h;
  try {
    const u = new URL(h);
    return u.protocol === "https:" || u.protocol === "http:" || u.protocol === "mailto:" ? u.href : "#";
  } catch {
    return "#";
  }
}

/* ============================================================================
 * THE CLOSE: a last call, then the footer
 * ========================================================================== */

export interface HgLink {
  label: string;
  href: string;
}

export interface HgFooterColumn {
  title: string;
  links: HgLink[];
}

export const HG_FOOTER_COLUMNS: HgFooterColumn[] = [
  {
    title: "Library",
    links: [
      { label: "All components", href: "/library" },
      { label: "Styles", href: "/styles" },
      { label: "Templates", href: "/templates" },
      { label: "Pricing", href: "/pricing" },
    ],
  },
  {
    title: "Kits",
    links: [
      { label: "Holo Gloss", href: "/styles/holo-gloss" },
      { label: "All kits", href: "/styles" },
      { label: "Docs", href: "/docs" },
      { label: "License", href: "/license" },
    ],
  },
  {
    title: "Elsewhere",
    links: [
      { label: "X", href: "https://x.com/viberdy" },
      { label: "GitHub", href: "https://github.com/viberdy-dev/ui" },
      { label: "Terms", href: "/terms" },
      { label: "Privacy", href: "/privacy" },
    ],
  },
];

const HG_CLOSE_CSS = `
.hg-close-pill{transition:transform .25s cubic-bezier(.2,.7,.2,1),background-color .3s,color .3s,border-color .3s}
.hg-close-pill:hover{transform:translateY(-1px)}
.hg-close-pill:focus-visible,.hg-foot a:focus-visible{outline:2px solid ${HG.cobalt} !important;outline-offset:3px;border-radius:6px}
.hg-foot a{background-image:linear-gradient(currentColor,currentColor);background-size:0 1px;background-repeat:no-repeat;background-position:0 100%;transition:background-size .3s cubic-bezier(.2,.7,.2,1),color .3s}
.hg-foot a:hover{background-size:100% 1px;color:${HG.ink}}
@media (prefers-reduced-motion: reduce){.hg-close-pill,.hg-foot a{transition:none}}
`;

/**
 * The end of the page. A last call to action set big, with the holo pill
 * between its words and two buttons (the main one to the plans, the other
 * to the sign-up); then the footer: the mark and a one-line promise, three
 * columns of real links, and the copyright with a way back up.
 */
export function HoloSiteFooter({
  brand = "viberdy",
  promise = "Premium React components in every style, built to be pasted.",
  call = "Ready when **you are.**",
  primary = { label: "See the plans", href: "#pricing" },
  secondary = { label: "Get drop emails", href: "#updates" },
  columns = HG_FOOTER_COLUMNS,
  legal = "© 2026 viberdy. All rights reserved.",
  className,
  style,
}: {
  brand?: string;
  promise?: string;
  /** **bold** marks the loud half. */
  call?: string;
  primary?: HgLink;
  secondary?: HgLink;
  columns?: HgFooterColumn[];
  legal?: string;
  className?: string;
  style?: CSSProperties;
}) {
  const parts = call.split("**");
  return (
    <footer className={"@container relative w-full " + (className ?? "")} style={{ background: HG.ground, color: HG.ink, fontFamily: HG_FONT, ...style }}>
      <style>{HG_CLOSE_CSS}</style>
      <div className="relative overflow-hidden px-[5.5cqw] pt-[7cqw] pb-[8cqw]">
        <div
          aria-hidden
          className="pointer-events-none absolute inset-0"
          style={{ background: "radial-gradient(40% 60% at 70% 40%, rgba(255,120,190,0.12), transparent 70%), radial-gradient(35% 50% at 85% 70%, rgba(60,90,255,0.12), transparent 70%)" }}
        />
        <h2 className="relative text-[clamp(40px,8.4cqw,128px)] leading-[0.98] tracking-[-0.045em]">
          {parts.map((t, i) => (
            <span key={i} style={{ fontWeight: i % 2 ? 700 : 300 }}>
              {t}
            </span>
          ))}
          <span aria-hidden className="ml-[0.18em] inline-block h-[0.62em] w-[1.9em] overflow-hidden rounded-full align-[-0.04em]">
            <HoloArt kind="foil" seed={8} className="h-full w-full" />
          </span>
        </h2>
        <div className="relative mt-[3.4cqw] flex flex-wrap gap-3">
          <a href={hgSafeHref(primary.href)} className="hg-close-pill inline-flex h-12 items-center gap-2 rounded-full px-6 text-[14px] font-semibold text-white" style={{ background: HG.ink }}>
            {primary.label}
            <HgAsterisk className="h-3.5 w-3.5" stroke={2.2} />
          </a>
          <a
            href={hgSafeHref(secondary.href)}
            className="hg-close-pill inline-flex h-12 items-center rounded-full px-6 text-[14px] font-semibold"
            style={{ border: "1px solid rgba(21,22,27,0.16)", background: "rgba(255,255,255,0.55)" }}
          >
            {secondary.label}
          </a>
        </div>
      </div>
      <div className="hg-foot px-[5.5cqw] pt-[4.5cqw] pb-8" style={{ background: HG.paper, borderTop: "1px solid rgba(21,22,27,0.08)" }}>
        <div className="grid grid-cols-2 gap-x-6 gap-y-10 @[760px]:grid-cols-[1.6fr_1fr_1fr_1fr]">
          <div className="col-span-2 @[760px]:col-span-1">
            <a href="#top" className="inline-flex items-center gap-2 text-[17px] font-semibold tracking-[-0.02em]" style={{ backgroundImage: "none" }}>
              <HgVMark className="h-[1.3em] w-[1.3em]" style={{ color: HG.cobalt }} />
              {brand}
            </a>
            <p className="mt-3 max-w-[26ch] text-[13.5px] leading-snug" style={{ color: HG.inkSoft }}>
              {promise}
            </p>
          </div>
          {columns.map((c) => (
            <nav key={c.title} aria-label={c.title}>
              <h3 className="text-[11px] font-semibold tracking-[0.16em] uppercase" style={{ color: HG.inkMute }}>
                {c.title}
              </h3>
              <ul className="mt-4 flex flex-col gap-2.5">
                {c.links.map((l) => (
                  <li key={l.label}>
                    <a href={hgSafeHref(l.href)} className="text-[13.5px]" style={{ color: "rgba(21,22,27,0.72)" }}>
                      {l.label}
                    </a>
                  </li>
                ))}
              </ul>
            </nav>
          ))}
        </div>
        <div className="mt-[4cqw] flex items-center justify-between gap-4 pt-6 text-[12px]" style={{ borderTop: "1px solid rgba(21,22,27,0.08)", color: HG.inkMute }}>
          <span>{legal}</span>
          <a href="#top" className="font-medium" style={{ color: "rgba(21,22,27,0.72)" }}>
            Back to top ↑
          </a>
        </div>
      </div>
    </footer>
  );
}

Free, yours to ship. Pro adds the heroes, WebGL backgrounds and complete style kits of Holo Gloss, from $6/month (launch price until 31 Oct).

Notes

About this section

Where the page ends, with somewhere to go. The last call is set at display size in a light and a bold half, with a short capsule of holo foil sitting in the line after it, over two faint pools of pink and cobalt. Under it, a black pill to the plans with a turning asterisk and an outlined pill to the sign-up. Then the footer proper on paper white: the V mark and the brand with a one-line promise, three columns of links (the library, the kits, and elsewhere: X, GitHub, terms and privacy) whose underlines draw in on hover, and a bottom row with the copyright and Back to top. Columns, links and every word are props.

Curator’s note

Links underline with a background-size transition, so the line draws from the left without touching layout; every href goes through the same allowlist as the plan cards.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

NewPro

Three glass plan cards with holo faces, dealt out of a pile into a row as the section scrolls into view, the featured one ringed in cobalt and lifted. Each card is a real offer: price, cadence, four lines and one link.

pricingplanspricing cards

A liquid-glass footer slab that rises over the section above as you scroll in, its meniscus edge sloshing on fast scrolls, with a pointer light, a droplet gliding between links and frost re-tinted to that section.

footersectionglass
Featured
NewPro

A scroll-driven hero in three acts: five iridescent glass cards fly in and land as a neat pile, scrolling opens the pile into a flip-book fan that leans back in real 3D, and scrolling on flies the camera into the fan while the headline lifts away. Every picture on the cards is ray-marched glass, painted in code.

hero3d cardsscroll animation