Encrypt Reveal Card — Free HTML CSS JS Cipher Hover Snippet

Encrypt Reveal Card · Cards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Cursor-following mask
A radial mask reveals only under the pointer.
CSS-variable reveal
--mx/--my move the porthole cheaply.
Churning ciphertext
Random hex-symbol string regenerates on move.
Throttled regeneration
Only ~25% of moves rescramble the text.
Scanner glow
A soft radial light lifts the revealed area.
Secured-to-decrypting
Lock dims as the stream fades in on hover.
Feathered edge
Mask gradient softens the porthole.
Monospace authenticity
Mono font sells the encrypted look.

About this UI Snippet

Encrypt Reveal Card — Cipher Characters Unmasked by the Cursor

Screenshot of the Encrypt Reveal Card snippet rendered live

The encrypt-reveal card is the security-themed effect popularized by Evervault's site: a card shows a locked icon at rest, but as you move your cursor over it, a field of constantly-churning encrypted characters becomes visible only inside a circular window that follows the pointer — as if you're decrypting the surface in real time. This snippet recreates it with plain HTML, CSS masks, and a small vanilla JavaScript generator.

A masked window that follows the cursor

The character stream fills the whole card but is revealed through a mask-image radial gradient centered on two CSS custom properties, --mx and --my. A pointermove handler writes the cursor's position into those variables, so the opaque part of the mask — a 140px circle — tracks under the pointer while everything outside it is masked to transparent. The effect is a moving porthole that exposes the "ciphertext" only where you point, which is the core illusion. Updating the reveal through CSS variables keeps the handler extremely cheap.

Churning ciphertext

The text is a random 900-character string drawn from a hex-and-symbol alphabet (ABCDEF0123456789...!@#$%) that looks like encrypted data. To make it feel alive rather than static, the handler regenerates the string on roughly a quarter of pointer moves (Math.random() < 0.25). That throttle is deliberate: regenerating on every move would be wasteful and visually too frantic, while occasional regeneration reads as data actively scrambling under your cursor. Because only the masked region is visible, you mostly perceive the churn happening right where you look.

A glow that reinforces the reveal

A second layer, .ev-mask, is a soft radial purple glow also centered on --mx/--my. It fades in on hover and sits over the card to make the revealed region feel lit, like a scanner passing over the surface. Pairing a glow with the character mask gives the reveal depth instead of looking like a flat cut-out.

The resting state

At rest the card shows a centered lock icon and an "Encrypted" label, and the character stream is at opacity: 0. On hover the stream fades to full opacity (within its mask) while the lock label dims to opacity: .15, so the card transitions from "secured" to "decrypting" as you interact. This clear before/after state communicates the metaphor without any copy.

Why masks instead of clip

Using mask-image rather than clip-path lets the reveal have a soft, feathered edge — the radial gradient fades from opaque to transparent over its radius, so the porthole blends smoothly into the hidden area rather than showing a hard circular cut. That softness is what makes it feel like a scanning beam.

Customizing it

Change the mask circle radius to widen or tighten the reveal, edit the CHARS alphabet (Katakana for a Matrix look, or 0/1 for binary), recolor the stream and glow, or adjust the 0.25 regeneration probability for calmer or busier churn. Swap the lock icon for your brand. Pair it with a glow input on a security product page or a background boxes hero.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA card shows a lock icon labeled Encrypted at rest.
  2. 2
    Move the cursor over itA circular window reveals churning encrypted characters.
  3. 3
    Move aroundThe reveal window follows the pointer like a scanner.
  4. 4
    Watch it churnThe ciphertext scrambles as you move.
  5. 5
    Change the alphabetEdit CHARS for binary, hex, or Katakana.
  6. 6
    Resize the windowAdjust the mask circle radius and glow.

Real-world uses

Common Use Cases

Security product pages
Pair with a glow input sign-in.
Privacy and encryption
Headline a feature beside a feature tabs showcase.
Developer tools
Set a technical mood near a terminal window.
Crypto and fintech
Combine with an animated grid background.
Hacker-themed sites
A subtler take than a full matrix rain.
CSS mask demos
A reference for cursor-driven mask reveals.

Got questions?

Frequently Asked Questions

The character stream fills the card but is shown through a mask-image radial gradient centered on the --mx and --my CSS variables. A pointermove handler writes the cursor position into those variables, so the opaque 140px circle of the mask tracks under the pointer while everything outside is masked to transparent — a moving porthole over the ciphertext.

The 900-character random string is regenerated on roughly a quarter of pointer moves (Math.random() < 0.25). Throttling it that way keeps the work cheap and the churn from looking too frantic, while still reading as data actively scrambling. Since only the masked region is visible, you perceive the churn right where you point.

mask-image with a radial gradient gives a soft, feathered edge — it fades from opaque to transparent across its radius, so the revealed area blends smoothly into the hidden surface. clip-path would produce a hard circular cut. The feathering is what makes the porthole feel like a scanning beam rather than a stencil.

No. The reveal moves purely by updating two CSS variables, which is very cheap, and the text only regenerates on about 25% of moves. There's no per-frame loop and no layout thrashing — the mask and glow are GPU-composited, so it stays smooth even on modest devices.

Keep the ciphertext in a ref and a pointermove handler that updates the --mx/--my inline style and occasionally regenerates the string. Avoid putting the churning text in state, since that would re-render constantly — write it directly to a ref'd element. The mask CSS ports as-is; in Tailwind use arbitrary mask-image values with the inline variables.

Encrypt Reveal Card — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Encrypt Reveal Card</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#06060c;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;flex-direction:column;gap:18px;padding:24px}
    
    .ev-card{position:relative;width:300px;height:300px;border-radius:20px;border:1px solid #23233a;background:#0b0b16;overflow:hidden;cursor:crosshair}
    
    /* The scrolling characters fill the card but are hidden, revealed only inside
       a radial mask centred on the cursor. */
    .ev-stream{position:absolute;inset:0;padding:8px;font-size:13px;line-height:1.35;letter-spacing:1px;color:#7c3aed;word-break:break-all;opacity:0;transition:opacity .2s;
      -webkit-mask-image:radial-gradient(140px circle at var(--mx,50%) var(--my,50%),#000 0%,transparent 60%);
      mask-image:radial-gradient(140px circle at var(--mx,50%) var(--my,50%),#000 0%,transparent 60%)}
    .ev-card:hover .ev-stream{opacity:1}
    
    .ev-mask{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .25s;
      background:radial-gradient(180px circle at var(--mx,50%) var(--my,50%),rgba(124,58,237,.35),transparent 60%)}
    .ev-card:hover .ev-mask{opacity:1}
    
    .ev-center{position:absolute;inset:0;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;transition:opacity .25s}
    .ev-card:hover .ev-center{opacity:.15}
    .ev-icon{width:54px;height:54px;border-radius:14px;background:rgba(124,58,237,.15);border:1px solid rgba(124,58,237,.4);display:flex;align-items:center;justify-content:center;font-size:24px}
    .ev-label{font-size:12px;letter-spacing:.15em;text-transform:uppercase;color:#a78bfa}
    .ev-hint{font-size:12px;color:#56566e;letter-spacing:.05em}
  </style>
</head>
<body>
  <div class="ev-stage">
    <article class="ev-card" id="evCard">
      <div class="ev-stream" id="evStream" aria-hidden="true"></div>
      <div class="ev-mask" id="evMask" aria-hidden="true"></div>
      <div class="ev-center">
        <div class="ev-icon">🔒</div>
        <span class="ev-label">Encrypted</span>
      </div>
    </article>
    <p class="ev-hint">Move your cursor over the card</p>
  </div>
  <script>
    var card = document.getElementById('evCard');
    var stream = document.getElementById('evStream');
    var CHARS = 'ABCDEF0123456789abcdef!@#$%&*<>{}[]/\\=+'.split('');
    
    function randomString(n) {
      var s = '';
      for (var i = 0; i < n; i++) s += CHARS[Math.floor(Math.random() * CHARS.length)];
      return s;
    }
    
    // Fill once, then re-randomise on each move so the "ciphertext" keeps churning.
    stream.textContent = randomString(900);
    
    card.addEventListener('pointermove', function (e) {
      var r = card.getBoundingClientRect();
      card.style.setProperty('--mx', (e.clientX - r.left) + 'px');
      card.style.setProperty('--my', (e.clientY - r.top) + 'px');
      // Only regenerate occasionally to keep it cheap.
      if (Math.random() < 0.25) stream.textContent = randomString(900);
    });
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Encrypt Reveal Card</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#06060c;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;flex-direction:column;gap:18px;padding:24px
    }

    .ev-card:hover .ev-stream {
      opacity:1
    }

    .ev-card:hover .ev-mask {
      opacity:1
    }

    .ev-card:hover .ev-center {
      opacity:.15
    }
  </style>
</head>
<body>
  <div class="ev-stage">
    <article class="ev-card relative w-[300px] h-[300px] rounded-[20px] border border-[#23233a] bg-[#0b0b16] overflow-hidden cursor-crosshair" id="evCard">
      <div class="ev-stream absolute inset-0 p-2 text-[13px] leading-[1.35] tracking-[1px] text-[#7c3aed] break-all opacity-0 [transition:opacity_.2s] [-webkit-mask-image:radial-gradient(140px_circle_at_var(--mx,50%)_var(--my,50%),#000_0%,transparent_60%)] [mask-image:radial-gradient(140px_circle_at_var(--mx,50%)_var(--my,50%),#000_0%,transparent_60%)]" id="evStream" aria-hidden="true"></div>
      <div class="ev-mask absolute inset-0 pointer-events-none opacity-0 [transition:opacity_.25s] [background:radial-gradient(180px_circle_at_var(--mx,50%)_var(--my,50%),rgba(124,58,237,.35),transparent_60%)]" id="evMask" aria-hidden="true"></div>
      <div class="ev-center absolute inset-0 z-[1] flex flex-col items-center justify-center gap-2.5 [transition:opacity_.25s]">
        <div class="w-[54px] h-[54px] rounded-[14px] bg-[rgba(124,58,237,.15)] border border-[rgba(124,58,237,.4)] flex items-center justify-center text-2xl">🔒</div>
        <span class="text-xs tracking-[.15em] uppercase text-[#a78bfa]">Encrypted</span>
      </div>
    </article>
    <p class="text-xs text-[#56566e] tracking-[.05em]">Move your cursor over the card</p>
  </div>
  <script>
    var card = document.getElementById('evCard');
    var stream = document.getElementById('evStream');
    var CHARS = 'ABCDEF0123456789abcdef!@#$%&*<>{}[]/\\=+'.split('');
    
    function randomString(n) {
      var s = '';
      for (var i = 0; i < n; i++) s += CHARS[Math.floor(Math.random() * CHARS.length)];
      return s;
    }
    
    // Fill once, then re-randomise on each move so the "ciphertext" keeps churning.
    stream.textContent = randomString(900);
    
    card.addEventListener('pointermove', function (e) {
      var r = card.getBoundingClientRect();
      card.style.setProperty('--mx', (e.clientX - r.left) + 'px');
      card.style.setProperty('--my', (e.clientY - r.top) + 'px');
      // Only regenerate occasionally to keep it cheap.
      if (Math.random() < 0.25) stream.textContent = randomString(900);
    });
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to EncryptRevealCard.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#06060c;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;flex-direction:column;gap:18px;padding:24px}

.ev-card{position:relative;width:300px;height:300px;border-radius:20px;border:1px solid #23233a;background:#0b0b16;overflow:hidden;cursor:crosshair}

/* The scrolling characters fill the card but are hidden, revealed only inside
   a radial mask centred on the cursor. */
.ev-stream{position:absolute;inset:0;padding:8px;font-size:13px;line-height:1.35;letter-spacing:1px;color:#7c3aed;word-break:break-all;opacity:0;transition:opacity .2s;
  -webkit-mask-image:radial-gradient(140px circle at var(--mx,50%) var(--my,50%),#000 0%,transparent 60%);
  mask-image:radial-gradient(140px circle at var(--mx,50%) var(--my,50%),#000 0%,transparent 60%)}
.ev-card:hover .ev-stream{opacity:1}

.ev-mask{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .25s;
  background:radial-gradient(180px circle at var(--mx,50%) var(--my,50%),rgba(124,58,237,.35),transparent 60%)}
.ev-card:hover .ev-mask{opacity:1}

.ev-center{position:absolute;inset:0;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;transition:opacity .25s}
.ev-card:hover .ev-center{opacity:.15}
.ev-icon{width:54px;height:54px;border-radius:14px;background:rgba(124,58,237,.15);border:1px solid rgba(124,58,237,.4);display:flex;align-items:center;justify-content:center;font-size:24px}
.ev-label{font-size:12px;letter-spacing:.15em;text-transform:uppercase;color:#a78bfa}
.ev-hint{font-size:12px;color:#56566e;letter-spacing:.05em}
`;

export default function EncryptRevealCard() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var card = document.getElementById('evCard');
    var stream = document.getElementById('evStream');
    var CHARS = 'ABCDEF0123456789abcdef!@#$%&*<>{}[]/\\=+'.split('');
    
    function randomString(n) {
      var s = '';
      for (var i = 0; i < n; i++) s += CHARS[Math.floor(Math.random() * CHARS.length)];
      return s;
    }
    
    // Fill once, then re-randomise on each move so the "ciphertext" keeps churning.
    stream.textContent = randomString(900);
    
    card.addEventListener('pointermove', function (e) {
      var r = card.getBoundingClientRect();
      card.style.setProperty('--mx', (e.clientX - r.left) + 'px');
      card.style.setProperty('--my', (e.clientY - r.top) + 'px');
      // Only regenerate occasionally to keep it cheap.
      if (Math.random() < 0.25) stream.textContent = randomString(900);
    });
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{css}</style>
      <div className="ev-stage">
        <article className="ev-card" id="evCard">
          <div className="ev-stream" id="evStream" aria-hidden="true"></div>
          <div className="ev-mask" id="evMask" aria-hidden="true"></div>
          <div className="ev-center">
            <div className="ev-icon">🔒</div>
            <span className="ev-label">Encrypted</span>
          </div>
        </article>
        <p className="ev-hint">Move your cursor over the card</p>
      </div>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function EncryptRevealCard() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var card = document.getElementById('evCard');
    var stream = document.getElementById('evStream');
    var CHARS = 'ABCDEF0123456789abcdef!@#$%&*<>{}[]/\\=+'.split('');
    
    function randomString(n) {
      var s = '';
      for (var i = 0; i < n; i++) s += CHARS[Math.floor(Math.random() * CHARS.length)];
      return s;
    }
    
    // Fill once, then re-randomise on each move so the "ciphertext" keeps churning.
    stream.textContent = randomString(900);
    
    card.addEventListener('pointermove', function (e) {
      var r = card.getBoundingClientRect();
      card.style.setProperty('--mx', (e.clientX - r.left) + 'px');
      card.style.setProperty('--my', (e.clientY - r.top) + 'px');
      // Only regenerate occasionally to keep it cheap.
      if (Math.random() < 0.25) stream.textContent = randomString(900);
    });
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{`
* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#06060c;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;flex-direction:column;gap:18px;padding:24px
}

.ev-card:hover .ev-stream {
  opacity:1
}

.ev-card:hover .ev-mask {
  opacity:1
}

.ev-card:hover .ev-center {
  opacity:.15
}
      `}</style>
      <div className="ev-stage">
        <article className="ev-card relative w-[300px] h-[300px] rounded-[20px] border border-[#23233a] bg-[#0b0b16] overflow-hidden cursor-crosshair" id="evCard">
          <div className="ev-stream absolute inset-0 p-2 text-[13px] leading-[1.35] tracking-[1px] text-[#7c3aed] break-all opacity-0 [transition:opacity_.2s] [-webkit-mask-image:radial-gradient(140px_circle_at_var(--mx,50%)_var(--my,50%),#000_0%,transparent_60%)] [mask-image:radial-gradient(140px_circle_at_var(--mx,50%)_var(--my,50%),#000_0%,transparent_60%)]" id="evStream" aria-hidden="true"></div>
          <div className="ev-mask absolute inset-0 pointer-events-none opacity-0 [transition:opacity_.25s] [background:radial-gradient(180px_circle_at_var(--mx,50%)_var(--my,50%),rgba(124,58,237,.35),transparent_60%)]" id="evMask" aria-hidden="true"></div>
          <div className="ev-center absolute inset-0 z-[1] flex flex-col items-center justify-center gap-2.5 [transition:opacity_.25s]">
            <div className="w-[54px] h-[54px] rounded-[14px] bg-[rgba(124,58,237,.15)] border border-[rgba(124,58,237,.4)] flex items-center justify-center text-2xl">🔒</div>
            <span className="text-xs tracking-[.15em] uppercase text-[#a78bfa]">Encrypted</span>
          </div>
        </article>
        <p className="text-xs text-[#56566e] tracking-[.05em]">Move your cursor over the card</p>
      </div>
    </>
  );
}
Vue
<template>
  <div class="ev-stage">
    <article class="ev-card" id="evCard">
      <div class="ev-stream" id="evStream" aria-hidden="true"></div>
      <div class="ev-mask" id="evMask" aria-hidden="true"></div>
      <div class="ev-center">
        <div class="ev-icon">🔒</div>
        <span class="ev-label">Encrypted</span>
      </div>
    </article>
    <p class="ev-hint">Move your cursor over the card</p>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

let card;
let stream;
var CHARS = 'ABCDEF0123456789abcdef!@#$%&*<>{}[]/\\=+'.split('');
function randomString(n) {
  var s = '';
  for (var i = 0; i < n; i++) s += CHARS[Math.floor(Math.random() * CHARS.length)];
  return s;
}

onMounted(() => {
  card = document.getElementById('evCard');
  stream = document.getElementById('evStream');
  // Fill once, then re-randomise on each move so the "ciphertext" keeps churning.
  stream.textContent = randomString(900);
  card.addEventListener('pointermove', function (e) {
    var r = card.getBoundingClientRect();
    card.style.setProperty('--mx', (e.clientX - r.left) + 'px');
    card.style.setProperty('--my', (e.clientY - r.top) + 'px');
    // Only regenerate occasionally to keep it cheap.
    if (Math.random() < 0.25) stream.textContent = randomString(900);
  });
});
</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#06060c;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;flex-direction:column;gap:18px;padding:24px}

.ev-card{position:relative;width:300px;height:300px;border-radius:20px;border:1px solid #23233a;background:#0b0b16;overflow:hidden;cursor:crosshair}

/* The scrolling characters fill the card but are hidden, revealed only inside
   a radial mask centred on the cursor. */
.ev-stream{position:absolute;inset:0;padding:8px;font-size:13px;line-height:1.35;letter-spacing:1px;color:#7c3aed;word-break:break-all;opacity:0;transition:opacity .2s;
  -webkit-mask-image:radial-gradient(140px circle at var(--mx,50%) var(--my,50%),#000 0%,transparent 60%);
  mask-image:radial-gradient(140px circle at var(--mx,50%) var(--my,50%),#000 0%,transparent 60%)}
.ev-card:hover .ev-stream{opacity:1}

.ev-mask{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .25s;
  background:radial-gradient(180px circle at var(--mx,50%) var(--my,50%),rgba(124,58,237,.35),transparent 60%)}
.ev-card:hover .ev-mask{opacity:1}

.ev-center{position:absolute;inset:0;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;transition:opacity .25s}
.ev-card:hover .ev-center{opacity:.15}
.ev-icon{width:54px;height:54px;border-radius:14px;background:rgba(124,58,237,.15);border:1px solid rgba(124,58,237,.4);display:flex;align-items:center;justify-content:center;font-size:24px}
.ev-label{font-size:12px;letter-spacing:.15em;text-transform:uppercase;color:#a78bfa}
.ev-hint{font-size:12px;color:#56566e;letter-spacing:.05em}
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-encrypt-reveal-card',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="ev-stage">
      <article class="ev-card" id="evCard">
        <div class="ev-stream" id="evStream" aria-hidden="true"></div>
        <div class="ev-mask" id="evMask" aria-hidden="true"></div>
        <div class="ev-center">
          <div class="ev-icon">🔒</div>
          <span class="ev-label">Encrypted</span>
        </div>
      </article>
      <p class="ev-hint">Move your cursor over the card</p>
    </div>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#06060c;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;flex-direction:column;gap:18px;padding:24px}
    
    .ev-card{position:relative;width:300px;height:300px;border-radius:20px;border:1px solid #23233a;background:#0b0b16;overflow:hidden;cursor:crosshair}
    
    /* The scrolling characters fill the card but are hidden, revealed only inside
       a radial mask centred on the cursor. */
    .ev-stream{position:absolute;inset:0;padding:8px;font-size:13px;line-height:1.35;letter-spacing:1px;color:#7c3aed;word-break:break-all;opacity:0;transition:opacity .2s;
      -webkit-mask-image:radial-gradient(140px circle at var(--mx,50%) var(--my,50%),#000 0%,transparent 60%);
      mask-image:radial-gradient(140px circle at var(--mx,50%) var(--my,50%),#000 0%,transparent 60%)}
    .ev-card:hover .ev-stream{opacity:1}
    
    .ev-mask{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .25s;
      background:radial-gradient(180px circle at var(--mx,50%) var(--my,50%),rgba(124,58,237,.35),transparent 60%)}
    .ev-card:hover .ev-mask{opacity:1}
    
    .ev-center{position:absolute;inset:0;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;transition:opacity .25s}
    .ev-card:hover .ev-center{opacity:.15}
    .ev-icon{width:54px;height:54px;border-radius:14px;background:rgba(124,58,237,.15);border:1px solid rgba(124,58,237,.4);display:flex;align-items:center;justify-content:center;font-size:24px}
    .ev-label{font-size:12px;letter-spacing:.15em;text-transform:uppercase;color:#a78bfa}
    .ev-hint{font-size:12px;color:#56566e;letter-spacing:.05em}
  `]
})
export class EncryptRevealCardComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var card = document.getElementById('evCard');
    var stream = document.getElementById('evStream');
    var CHARS = 'ABCDEF0123456789abcdef!@#$%&*<>{}[]/\\=+'.split('');
    
    function randomString(n) {
      var s = '';
      for (var i = 0; i < n; i++) s += CHARS[Math.floor(Math.random() * CHARS.length)];
      return s;
    }
    
    // Fill once, then re-randomise on each move so the "ciphertext" keeps churning.
    stream.textContent = randomString(900);
    
    card.addEventListener('pointermove', function (e) {
      var r = card.getBoundingClientRect();
      card.style.setProperty('--mx', (e.clientX - r.left) + 'px');
      card.style.setProperty('--my', (e.clientY - r.top) + 'px');
      // Only regenerate occasionally to keep it cheap.
      if (Math.random() < 0.25) stream.textContent = randomString(900);
    });

    // Bind inline-handler functions to the component so the template can call them
    Object.assign(this, { randomString });
  }
}