{"product_id":"crystal-tornado-custom-lyric-text-engraving","title":"Swirling Memories — Custom Lyric \u0026 Text Engraving","description":"\u003cp\u003eYour Words, and Memories Etched Inside Crystal\u003c\/p\u003e\n\u003cp\u003eHave you ever had song lyrics, or a special movie quote swirled around in your head?  A special phrase or sentiments share with by someone important to you?  Here is a one-of-a-kind keepsake where your lyrics, poem, or personal message is laser-engraved \u003cem\u003einside\u003c\/em\u003e solid optical crystal in a stunning 3D tornado spiral. Every word wraps around the vortex — readable from any angle, glowing beautifully under light.\u003c\/p\u003e\n\u003ch3\u003eHow It Works\u003c\/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cstrong\u003eCustomise your design\u003c\/strong\u003e — use our online designer tool to enter your text and preview the tornado in 3D before ordering.\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eWe confirm your design\u003c\/strong\u003e — you'll receive a design confirmation email within 24 hours.\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eWe engrave \u0026amp; ship\u003c\/strong\u003e — your crystal is engraved using a UV sub-surface laser (xTool F2 Ultra) and ships in 3–5 business days, gift-boxed and ready to give.\u003c\/li\u003e\n\u003c\/ol\u003e\n\u003ch3\u003ePerfect For\u003c\/h3\u003e\n\u003cul\u003e\n\u003cli\u003eSong lyrics from a first dance, favourite song, or meaningful moment\u003c\/li\u003e\n\u003cli\u003eWedding vows, anniversary messages, or love letters\u003c\/li\u003e\n\u003cli\u003ePoems, quotes, or personal tributes\u003c\/li\u003e\n\u003cli\u003eMemorial pieces with names, dates, and memories\u003c\/li\u003e\n\u003c\/ul\u003e\n\u003ch3\u003eWhat's Included\u003c\/h3\u003e\n\u003cul\u003e\n\u003cli\u003eOptically clear K9 crystal with sub-surface UV laser engraving\u003c\/li\u003e\n\u003cli\u003eGift box with ribbon\u003c\/li\u003e\n\u003cli\u003eLED illuminated base (displays the crystal beautifully)\u003c\/li\u003e\n\u003cli\u003eCare card\u003c\/li\u003e\n\u003c\/ul\u003e\n\u003ch3\u003eCrystal Sizes\u003c\/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cstrong\u003eClassic (50×50×80mm) — $89\u003c\/strong\u003e: Perfect for a bedside table, shelf, or desk. Holds up to ~60 words in a full tornado spiral.\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eGrand (70×70×100mm) — $129\u003c\/strong\u003e: A statement piece with larger te \u003cmeta charset=\"UTF-8\"\u003e \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e \u003clink rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\"\u003e \u003clink href=\"https:\/\/fonts.googleapis.com\/css2?family=Cinzel:wght@400;600;700\u0026amp;family=Inter:wght@300;400;500;600\u0026amp;family=JetBrains+Mono:wght@400;500\u0026amp;display=swap\" rel=\"stylesheet\"\u003e\n\u003c\/li\u003e\n\u003c\/ul\u003e\n\u003cstyle\u003e\n  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n  :root {\n    --void:      #0a0b0f;\n    --surface:   #0e1628;\n    --elevated:  #1a2744;\n    --border:    #243450;\n    --blue:      #4a9eff;\n    --ice:       #a8d4ff;\n    --white:     #ffffff;\n    --gold:      #c8a96e;\n    --gold-dim:  #a08050;\n    --muted:     #4a6080;\n    --error:     #ff6b6b;\n    --success:   #4affb0;\n    --font-display: 'Cinzel', serif;\n    --font-body:    'Inter', sans-serif;\n    --font-mono:    'JetBrains Mono', monospace;\n    --radius:    8px;\n    --radius-lg: 16px;\n  }\n\n  html { scroll-behavior: smooth; }\n\n  body {\n    background: var(--void);\n    color: var(--white);\n    font-family: var(--font-body);\n    font-size: 14px;\n    line-height: 1.6;\n    min-height: 100vh;\n    overflow-x: hidden;\n  }\n\n  \/* ── HERO ─────────────────────────────────────────────────────────── *\/\n  .hero {\n    position: relative;\n    text-align: center;\n    padding: 64px 24px 48px;\n    overflow: hidden;\n  }\n  .hero-bg {\n    position: absolute; inset: 0;\n    background: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(74,158,255,0.12) 0%, transparent 70%);\n    pointer-events: none;\n  }\n  #hero-canvas {\n    position: absolute; inset: 0;\n    width: 100%; height: 100%;\n    opacity: 0.35;\n    pointer-events: none;\n  }\n  .hero-content { position: relative; z-index: 1; }\n  .hero-eyebrow {\n    font-family: var(--font-mono);\n    font-size: 11px;\n    letter-spacing: 0.25em;\n    color: var(--blue);\n    text-transform: uppercase;\n    margin-bottom: 16px;\n  }\n  .hero-title {\n    font-family: var(--font-display);\n    font-size: clamp(32px, 6vw, 64px);\n    font-weight: 700;\n    letter-spacing: 0.04em;\n    line-height: 1.1;\n    color: var(--white);\n    margin-bottom: 16px;\n  }\n  .hero-title span {\n    background: linear-gradient(135deg, var(--blue), var(--ice));\n    -webkit-background-clip: text;\n    -webkit-text-fill-color: transparent;\n    background-clip: text;\n  }\n  .hero-sub {\n    font-size: 16px;\n    color: var(--ice);\n    font-weight: 300;\n    max-width: 520px;\n    margin: 0 auto 32px;\n    opacity: 0.85;\n  }\n  .hero-badges {\n    display: flex;\n    gap: 12px;\n    justify-content: center;\n    flex-wrap: wrap;\n  }\n  .badge {\n    font-family: var(--font-mono);\n    font-size: 11px;\n    padding: 5px 12px;\n    border: 1px solid var(--border);\n    border-radius: 20px;\n    color: var(--ice);\n    letter-spacing: 0.05em;\n    background: rgba(14,22,40,0.6);\n  }\n\n  \/* ── MAIN LAYOUT ──────────────────────────────────────────────────── *\/\n  .main {\n    max-width: 1100px;\n    margin: 0 auto;\n    padding: 0 24px 80px;\n    display: grid;\n    grid-template-columns: 380px 1fr;\n    gap: 24px;\n    align-items: start;\n  }\n  @media (max-width: 820px) {\n    .main { grid-template-columns: 1fr; }\n  }\n\n  \/* ── PANEL ────────────────────────────────────────────────────────── *\/\n  .panel {\n    background: var(--surface);\n    border: 1px solid var(--border);\n    border-radius: var(--radius-lg);\n    padding: 24px;\n  }\n  .panel-title {\n    font-family: var(--font-display);\n    font-size: 13px;\n    letter-spacing: 0.12em;\n    text-transform: uppercase;\n    color: var(--ice);\n    margin-bottom: 20px;\n    padding-bottom: 12px;\n    border-bottom: 1px solid var(--border);\n  }\n\n  \/* ── FORM ELEMENTS ────────────────────────────────────────────────── *\/\n  label {\n    display: block;\n    font-size: 11px;\n    font-weight: 500;\n    letter-spacing: 0.08em;\n    text-transform: uppercase;\n    color: var(--ice);\n    margin-bottom: 6px;\n  }\n  .field { margin-bottom: 18px; }\n\n  textarea, input[type=text], input[type=email], input[type=tel], select {\n    width: 100%;\n    background: var(--elevated);\n    border: 1px solid var(--border);\n    border-radius: var(--radius);\n    color: var(--white);\n    font-family: var(--font-body);\n    font-size: 14px;\n    padding: 10px 14px;\n    outline: none;\n    transition: border-color 0.2s, box-shadow 0.2s;\n    appearance: none;\n  }\n  textarea:focus, input:focus, select:focus {\n    border-color: var(--blue);\n    box-shadow: 0 0 0 3px rgba(74,158,255,0.15);\n  }\n  textarea {\n    font-family: var(--font-body);\n    resize: vertical;\n    min-height: 140px;\n    line-height: 1.6;\n  }\n  .char-count {\n    font-family: var(--font-mono);\n    font-size: 11px;\n    color: var(--muted);\n    text-align: right;\n    margin-top: 4px;\n  }\n  .char-count.warn { color: var(--gold); }\n\n  \/* Size selector *\/\n  .size-grid {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 10px;\n    margin-bottom: 18px;\n  }\n  .size-card {\n    background: var(--elevated);\n    border: 1.5px solid var(--border);\n    border-radius: var(--radius);\n    padding: 14px 12px;\n    cursor: pointer;\n    transition: border-color 0.2s, background 0.2s;\n    text-align: center;\n  }\n  .size-card:hover { border-color: var(--blue); }\n  .size-card.active {\n    border-color: var(--blue);\n    background: rgba(74,158,255,0.1);\n    box-shadow: 0 0 20px rgba(74,158,255,0.1);\n  }\n  .size-card .size-dims {\n    font-family: var(--font-mono);\n    font-size: 13px;\n    font-weight: 500;\n    color: var(--white);\n    margin-bottom: 4px;\n  }\n  .size-card .size-label {\n    font-size: 11px;\n    color: var(--muted);\n  }\n  .size-card .size-price {\n    font-family: var(--font-display);\n    font-size: 16px;\n    color: var(--gold);\n    margin-top: 8px;\n  }\n\n  \/* Buttons *\/\n  .btn {\n    display: block;\n    width: 100%;\n    padding: 14px;\n    border: none;\n    border-radius: var(--radius);\n    font-family: var(--font-display);\n    font-size: 14px;\n    letter-spacing: 0.1em;\n    font-weight: 600;\n    cursor: pointer;\n    transition: all 0.2s;\n    text-align: center;\n    text-decoration: none;\n  }\n  .btn-primary {\n    background: linear-gradient(135deg, #c8a96e, #e8c98e);\n    color: #0a0b0f;\n  }\n  .btn-primary:hover {\n    background: linear-gradient(135deg, #e8c98e, #c8a96e);\n    transform: translateY(-1px);\n    box-shadow: 0 8px 24px rgba(200,169,110,0.3);\n  }\n  .btn-primary:disabled {\n    opacity: 0.4;\n    cursor: not-allowed;\n    transform: none;\n    box-shadow: none;\n  }\n  .btn-ghost {\n    background: transparent;\n    border: 1px solid var(--border);\n    color: var(--ice);\n    margin-top: 10px;\n  }\n  .btn-ghost:hover {\n    border-color: var(--blue);\n    color: var(--blue);\n  }\n\n  \/* ── 3D PREVIEW ───────────────────────────────────────────────────── *\/\n  .preview-panel {\n    background: var(--surface);\n    border: 1px solid var(--border);\n    border-radius: var(--radius-lg);\n    overflow: hidden;\n    position: sticky;\n    top: 24px;\n  }\n  .preview-header {\n    padding: 16px 20px;\n    border-bottom: 1px solid var(--border);\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n  }\n  .preview-title {\n    font-family: var(--font-display);\n    font-size: 12px;\n    letter-spacing: 0.12em;\n    text-transform: uppercase;\n    color: var(--ice);\n  }\n  .preview-hint {\n    font-size: 11px;\n    color: var(--muted);\n    font-family: var(--font-mono);\n  }\n  #preview-canvas {\n    width: 100%;\n    aspect-ratio: 1;\n    cursor: grab;\n    display: block;\n    background: radial-gradient(ellipse at 50% 40%, #0d1f3c 0%, #0a0b0f 70%);\n  }\n  #preview-canvas:active { cursor: grabbing; }\n  .preview-footer {\n    padding: 12px 20px;\n    border-top: 1px solid var(--border);\n    display: flex;\n    gap: 16px;\n    font-family: var(--font-mono);\n    font-size: 11px;\n    color: var(--muted);\n  }\n  .preview-stat span { color: var(--ice); }\n\n  \/* ── ORDER MODAL ──────────────────────────────────────────────────── *\/\n  .modal-overlay {\n    position: fixed; inset: 0;\n    background: rgba(10,11,15,0.85);\n    backdrop-filter: blur(6px);\n    z-index: 100;\n    display: flex;\n    align-items: flex-end;\n    justify-content: center;\n    opacity: 0;\n    pointer-events: none;\n    transition: opacity 0.3s;\n  }\n  .modal-overlay.open {\n    opacity: 1;\n    pointer-events: all;\n  }\n  .modal {\n    background: var(--surface);\n    border: 1px solid var(--border);\n    border-radius: var(--radius-lg) var(--radius-lg) 0 0;\n    width: 100%;\n    max-width: 580px;\n    padding: 32px;\n    transform: translateY(40px);\n    transition: transform 0.3s ease;\n    max-height: 90vh;\n    overflow-y: auto;\n  }\n  .modal-overlay.open .modal { transform: translateY(0); }\n  .modal-title {\n    font-family: var(--font-display);\n    font-size: 22px;\n    letter-spacing: 0.06em;\n    margin-bottom: 6px;\n  }\n  .modal-sub {\n    color: var(--ice);\n    font-size: 13px;\n    margin-bottom: 24px;\n    opacity: 0.8;\n  }\n  .order-summary {\n    background: var(--elevated);\n    border: 1px solid var(--border);\n    border-radius: var(--radius);\n    padding: 14px 16px;\n    margin-bottom: 20px;\n    font-family: var(--font-mono);\n    font-size: 12px;\n  }\n  .order-summary-row {\n    display: flex;\n    justify-content: space-between;\n    padding: 3px 0;\n  }\n  .order-summary-row .key { color: var(--muted); }\n  .order-summary-row .val { color: var(--white); }\n  .order-summary-row.total { border-top: 1px solid var(--border); margin-top: 8px; padding-top: 10px; }\n  .order-summary-row.total .val { color: var(--gold); font-size: 15px; }\n  .form-row {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 12px;\n  }\n  .modal-close {\n    position: absolute;\n    top: 16px; right: 16px;\n    background: none;\n    border: none;\n    color: var(--muted);\n    font-size: 20px;\n    cursor: pointer;\n    padding: 4px 8px;\n  }\n  .modal-close:hover { color: var(--white); }\n\n  \/* Status messages *\/\n  .status {\n    padding: 12px 16px;\n    border-radius: var(--radius);\n    font-size: 13px;\n    margin-top: 12px;\n    display: none;\n  }\n  .status.success { background: rgba(74,255,176,0.1); border: 1px solid rgba(74,255,176,0.3); color: var(--success); display: block; }\n  .status.error   { background: rgba(255,107,107,0.1); border: 1px solid rgba(255,107,107,0.3); color: var(--error); display: block; }\n\n  \/* Divider *\/\n  .divider { border: none; border-top: 1px solid var(--border); margin: 20px 0; }\n\n  \/* Spinner *\/\n  .spinner {\n    display: inline-block;\n    width: 14px; height: 14px;\n    border: 2px solid rgba(10,11,15,0.3);\n    border-top-color: #0a0b0f;\n    border-radius: 50%;\n    animation: spin 0.7s linear infinite;\n    vertical-align: middle;\n    margin-right: 6px;\n  }\n  @keyframes spin { to { transform: rotate(360deg); } }\n\n  \/* Footer note *\/\n  .footer-note {\n    text-align: center;\n    color: var(--muted);\n    font-size: 12px;\n    padding: 32px 24px;\n    border-top: 1px solid var(--border);\n  }\n  .footer-note a { color: var(--ice); text-decoration: none; }\n\n  select option { background: var(--elevated); }\n\u003c\/style\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003c!-- ── HERO ──────────────────────────────────────────────────────────── --\u003e\n\u003csection class=\"hero\"\u003e\u003ccanvas id=\"hero-canvas\"\u003e\u003c\/canvas\u003e\n\u003cdiv class=\"hero-bg\"\u003e\u003cbr\u003e\u003c\/div\u003e\n\u003cdiv class=\"hero-content\"\u003e\n\u003cp class=\"hero-eyebrow\"\u003eEtches Memories · Bespoke Crystal Engraving\u003c\/p\u003e\n\u003ch1 class=\"hero-title\"\u003eWords Etched\u003cbr\u003e\u003cspan\u003eInside Crystal\u003c\/span\u003e\n\u003c\/h1\u003e\n\u003cp class=\"hero-sub\"\u003eEnter your lyrics, poem, or message. Watch it spiral into a 3D tornado of light — then order your one-of-a-kind engraved crystal.\u003c\/p\u003e\n\u003cdiv class=\"hero-badges\"\u003e\n\u003cspan class=\"badge\"\u003eUV Sub-Surface Laser\u003c\/span\u003e \u003cspan class=\"badge\"\u003exTool F2 Ultra\u003c\/span\u003e \u003cspan class=\"badge\"\u003eShips in 3–5 Days\u003c\/span\u003e \u003cspan class=\"badge\"\u003eGift-Ready Packaging\u003c\/span\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/section\u003e\n\u003c!-- ── MAIN ───────────────────────────────────────────────────────────── --\u003e\u003cmain class=\"main\"\u003e\u003c!-- CONFIG PANEL --\u003e\n\u003cdiv\u003e\n\u003cdiv class=\"panel\"\u003e\n\u003cp class=\"panel-title\"\u003eDesign Your Crystal\u003c\/p\u003e\n\u003c!-- Crystal size --\u003e\n\u003cdiv class=\"field\"\u003e\n\u003clabel\u003eCrystal Size\u003c\/label\u003e\n\u003cdiv class=\"size-grid\"\u003e\n\u003cdiv class=\"size-card active\" data-size=\"50x50x80\"\u003e\n\u003cdiv class=\"size-dims\"\u003e50×50×80mm\u003c\/div\u003e\n\u003cdiv class=\"size-label\"\u003eClassic Portrait\u003c\/div\u003e\n\u003cdiv class=\"size-price\"\u003e$89\u003c\/div\u003e\n\u003c\/div\u003e\n\u003cdiv class=\"size-card\" data-size=\"70x70x100\"\u003e\n\u003cdiv class=\"size-dims\"\u003e70×70×100mm\u003c\/div\u003e\n\u003cdiv class=\"size-label\"\u003eGrand Statement\u003c\/div\u003e\n\u003cdiv class=\"size-price\"\u003e$129\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c!-- Text input --\u003e\n\u003cdiv class=\"field\"\u003e\n\u003clabel\u003eYour Lyrics or Message\u003c\/label\u003e \u003ctextarea id=\"lyrics\" placeholder=\"I don't know how you do what you do\nI'm so in love with you\nIt just keeps getting better…\"\u003e\u003c\/textarea\u003e\n\u003cdiv class=\"char-count\" id=\"char-count\"\u003e0 words\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c!-- Gift message --\u003e\n\u003cdiv class=\"field\"\u003e\n\u003clabel\u003eGift Message \u003cspan style=\"color: var(--muted); text-transform: none; font-size: 10px;\"\u003e(optional, printed on card)\u003c\/span\u003e\u003c\/label\u003e \u003ctextarea id=\"gift-msg\" placeholder=\"To Sarah, with all my love…\" style=\"min-height: 70px;\"\u003e\u003c\/textarea\u003e\n\u003c\/div\u003e\n\u003chr class=\"divider\"\u003e\n\u003cbutton class=\"btn btn-primary\" id=\"order-btn\" disabled\u003e Preview \u0026amp; Order → \u003c\/button\u003e\n\u003cp style=\"text-align: center; font-size: 11px; color: var(--muted); margin-top: 10px;\"\u003eNo payment until we confirm your design by email\u003c\/p\u003e\n\u003c\/div\u003e\n\u003c!-- How it works --\u003e\n\u003cdiv class=\"panel\" style=\"margin-top: 16px;\"\u003e\n\u003cp class=\"panel-title\"\u003eHow It Works\u003c\/p\u003e\n\u003cdiv style=\"display: flex; flex-direction: column; gap: 14px;\"\u003e\n\u003cdiv style=\"display: flex; gap: 12px; align-items: flex-start;\"\u003e\n\u003cdiv style=\"width: 24px; height: 24px; border-radius: 50%; background: rgba(74,158,255,0.15); border: 1px solid var(--blue); display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--font-mono); font-size: 11px; color: var(--blue);\"\u003e1\u003c\/div\u003e\n\u003cdiv\u003e\n\u003cdiv style=\"font-weight: 500; margin-bottom: 2px;\"\u003eEnter your text\u003c\/div\u003e\n\u003cdiv style=\"color: var(--muted); font-size: 12px;\"\u003eLyrics, a poem, names and dates — anything meaningful\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003cdiv style=\"display: flex; gap: 12px; align-items: flex-start;\"\u003e\n\u003cdiv style=\"width: 24px; height: 24px; border-radius: 50%; background: rgba(74,158,255,0.15); border: 1px solid var(--blue); display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--font-mono); font-size: 11px; color: var(--blue);\"\u003e2\u003c\/div\u003e\n\u003cdiv\u003e\n\u003cdiv style=\"font-weight: 500; margin-bottom: 2px;\"\u003ePreview in 3D\u003c\/div\u003e\n\u003cdiv style=\"color: var(--muted); font-size: 12px;\"\u003eRotate the crystal to see your words spiral inside\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003cdiv style=\"display: flex; gap: 12px; align-items: flex-start;\"\u003e\n\u003cdiv style=\"width: 24px; height: 24px; border-radius: 50%; background: rgba(74,158,255,0.15); border: 1px solid var(--blue); display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--font-mono); font-size: 11px; color: var(--blue);\"\u003e3\u003c\/div\u003e\n\u003cdiv\u003e\n\u003cdiv style=\"font-weight: 500; margin-bottom: 2px;\"\u003eWe confirm \u0026amp; engrave\u003c\/div\u003e\n\u003cdiv style=\"color: var(--muted); font-size: 12px;\"\u003eYou'll receive a design confirmation email within 24 hours\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003cdiv style=\"display: flex; gap: 12px; align-items: flex-start;\"\u003e\n\u003cdiv style=\"width: 24px; height: 24px; border-radius: 50%; background: rgba(74,158,255,0.15); border: 1px solid var(--blue); display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: var(--font-mono); font-size: 11px; color: var(--blue);\"\u003e4\u003c\/div\u003e\n\u003cdiv\u003e\n\u003cdiv style=\"font-weight: 500; margin-bottom: 2px;\"\u003eShips in 3–5 days\u003c\/div\u003e\n\u003cdiv style=\"color: var(--muted); font-size: 12px;\"\u003eGift-boxed with an LED base and care card\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c!-- PREVIEW PANEL --\u003e\n\u003cdiv class=\"preview-panel\"\u003e\n\u003cdiv class=\"preview-header\"\u003e\n\u003cspan class=\"preview-title\"\u003eLive 3D Preview\u003c\/span\u003e \u003cspan class=\"preview-hint\"\u003edrag to rotate · scroll to zoom\u003c\/span\u003e\n\u003c\/div\u003e\n\u003ccanvas id=\"preview-canvas\" width=\"600\" height=\"600\"\u003e\u003c\/canvas\u003e\n\u003cdiv class=\"preview-footer\"\u003e\n\u003cdiv class=\"preview-stat\"\u003eWords: \u003cspan id=\"stat-words\"\u003e0\u003c\/span\u003e\n\u003c\/div\u003e\n\u003cdiv class=\"preview-stat\"\u003eSize: \u003cspan id=\"stat-size\"\u003e50×50×80mm\u003c\/span\u003e\n\u003c\/div\u003e\n\u003cdiv class=\"preview-stat\"\u003eTurns: \u003cspan id=\"stat-turns\"\u003e—\u003c\/span\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/main\u003e\u003c!-- ── ORDER MODAL ────────────────────────────────────────────────────── --\u003e\n\u003cdiv class=\"modal-overlay\" id=\"modal-overlay\"\u003e\n\u003cdiv class=\"modal\" style=\"position: relative;\"\u003e\n\u003cbutton class=\"modal-close\"\u003e✕\u003c\/button\u003e\n\u003ch2 class=\"modal-title\"\u003eComplete Your Order\u003c\/h2\u003e\n\u003cp class=\"modal-sub\"\u003eWe'll email you a design confirmation before charging anything.\u003c\/p\u003e\n\u003cdiv class=\"order-summary\" id=\"order-summary\"\u003e\n\u003cdiv class=\"order-summary-row\"\u003e\n\u003cspan class=\"key\"\u003eCrystal\u003c\/span\u003e\u003cspan class=\"val\" id=\"sum-size\"\u003e—\u003c\/span\u003e\n\u003c\/div\u003e\n\u003cdiv class=\"order-summary-row\"\u003e\n\u003cspan class=\"key\"\u003eWords\u003c\/span\u003e\u003cspan class=\"val\" id=\"sum-words\"\u003e—\u003c\/span\u003e\n\u003c\/div\u003e\n\u003cdiv class=\"order-summary-row\"\u003e\n\u003cspan class=\"key\"\u003eGift message\u003c\/span\u003e\u003cspan class=\"val\" id=\"sum-gift\"\u003eNone\u003c\/span\u003e\n\u003c\/div\u003e\n\u003cdiv class=\"order-summary-row total\"\u003e\n\u003cspan class=\"key\"\u003eEstimated total\u003c\/span\u003e\u003cspan class=\"val\" id=\"sum-price\"\u003e—\u003c\/span\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003cdiv class=\"form-row\"\u003e\n\u003cdiv class=\"field\"\u003e\n\u003clabel\u003eFirst Name\u003c\/label\u003e \u003cinput type=\"text\" id=\"fname\" placeholder=\"Sarah\"\u003e\n\u003c\/div\u003e\n\u003cdiv class=\"field\"\u003e\n\u003clabel\u003eLast Name\u003c\/label\u003e \u003cinput type=\"text\" id=\"lname\" placeholder=\"Johnson\"\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003cdiv class=\"field\"\u003e\n\u003clabel\u003eEmail\u003c\/label\u003e \u003cinput type=\"email\" id=\"email\" placeholder=\"sarah@example.com\"\u003e\n\u003c\/div\u003e\n\u003cdiv class=\"field\"\u003e\n\u003clabel\u003ePhone \u003cspan style=\"color: var(--muted); text-transform: none; font-size: 10px;\"\u003e(optional)\u003c\/span\u003e\u003c\/label\u003e \u003cinput type=\"tel\" id=\"phone\" placeholder=\"+1 (555) 000-0000\"\u003e\n\u003c\/div\u003e\n\u003cdiv class=\"field\"\u003e\n\u003clabel\u003eShipping Address\u003c\/label\u003e \u003cinput type=\"text\" id=\"address\" placeholder=\"123 Main St, City, State, ZIP\"\u003e\n\u003c\/div\u003e\n\u003cdiv class=\"field\"\u003e\n\u003clabel\u003eSpecial Instructions \u003cspan style=\"color: var(--muted); text-transform: none; font-size: 10px;\"\u003e(optional)\u003c\/span\u003e\u003c\/label\u003e \u003ctextarea id=\"notes\" style=\"min-height: 60px;\" placeholder=\"Rush order, specific occasion date, etc.\"\u003e\u003c\/textarea\u003e\n\u003c\/div\u003e\n\u003cbutton class=\"btn btn-primary\" id=\"submit-btn\"\u003e Send Order Request \u003c\/button\u003e \u003cbutton class=\"btn btn-ghost\"\u003e← Back to Preview\u003c\/button\u003e\n\u003cdiv class=\"status\" id=\"order-status\"\u003e\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003c!-- ── FOOTER ─────────────────────────────────────────────────────────── --\u003e\u003cfooter class=\"footer-note\"\u003e\n\u003cp\u003eEtches Memories · Bespoke Crystal Engraving · \u003ca href=\"mailto:jasonetchesmemories@gmail.com\"\u003ejasonetchesmemories@gmail.com\u003c\/a\u003e\u003c\/p\u003e\n\u003cp style=\"margin-top: 6px;\"\u003eAll designs are reviewed before production. Payment collected on confirmation.\u003c\/p\u003e\n\u003c\/footer\u003e\u003cscript\u003e\n\/\/ ── STATE ─────────────────────────────────────────────────────────────────\nlet selectedSize = '50x50x80';\nlet selectedPrice = 89;\nlet words3D = [];\nlet rotX = -0.25, rotY = 0.4, zoom = 1.3;\nlet dragging = false, lastX, lastY;\nlet autoRotating = true, autoTimer = null;\nlet debounceTimer = null;\n\nconst SIZES = {\n  '50x50x80':  { label:'50×50×80mm', maxR:21,  minR:1.5, height:72, turns:5.875 },\n  '70x70x100': { label:'70×70×100mm', maxR:30, minR:1.5, height:90, turns:7.0   }\n};\n\n\/\/ ── HERO PARTICLES ────────────────────────────────────────────────────────\n(function(){\n  const c = document.getElementById('hero-canvas');\n  const ctx = c.getContext('2d');\n  let W, H, pts=[];\n  function resize(){ W=c.width=c.offsetWidth; H=c.height=c.offsetHeight; }\n  function init(){\n    pts=[];\n    for(let i=0;i\u003c80;i++) pts.push({\n      x:Math.random()*W, y:Math.random()*H,\n      vx:(Math.random()-.5)*0.3, vy:(Math.random()-.5)*0.3,\n      r:Math.random()*1.5+0.3, a:Math.random()\n    });\n  }\n  function draw(){\n    ctx.clearRect(0,0,W,H);\n    pts.forEach(p=\u003e{\n      p.x+=p.vx; p.y+=p.vy;\n      if(p.x\u003c0)p.x=W; if(p.x\u003eW)p.x=0;\n      if(p.y\u003c0)p.y=H; if(p.y\u003eH)p.y=0;\n      ctx.beginPath();\n      ctx.arc(p.x,p.y,p.r,0,Math.PI*2);\n      ctx.fillStyle=`rgba(74,158,255,${p.a*0.6})`;\n      ctx.fill();\n    });\n    requestAnimationFrame(draw);\n  }\n  resize(); init(); draw();\n  window.addEventListener('resize',()=\u003e{ resize(); init(); });\n})();\n\n\/\/ ── PREVIEW CANVAS ────────────────────────────────────────────────────────\nconst cv = document.getElementById('preview-canvas');\nconst ctx2 = cv.getContext('2d');\n\ncv.addEventListener('mousedown', e=\u003e{ dragging=true; lastX=e.clientX; lastY=e.clientY; stopAuto(); });\ncv.addEventListener('mousemove', e=\u003e{\n  if(!dragging) return;\n  rotY+=(e.clientX-lastX)*0.007; rotX+=(e.clientY-lastY)*0.007;\n  lastX=e.clientX; lastY=e.clientY; drawPreview();\n});\ncv.addEventListener('mouseup', ()=\u003e{ dragging=false; scheduleAuto(); });\ncv.addEventListener('mouseleave',()=\u003e{ dragging=false; scheduleAuto(); });\ncv.addEventListener('wheel',e=\u003e{ zoom=Math.max(0.6,Math.min(2.8,zoom-e.deltaY*0.001)); drawPreview(); e.preventDefault(); },{passive:false});\n\n\/\/ Touch\ncv.addEventListener('touchstart',e=\u003e{\n  dragging=true;\n  lastX=e.touches[0].clientX; lastY=e.touches[0].clientY;\n  stopAuto(); e.preventDefault();\n},{passive:false});\ncv.addEventListener('touchmove',e=\u003e{\n  if(!dragging)return;\n  rotY+=(e.touches[0].clientX-lastX)*0.007;\n  rotX+=(e.touches[0].clientY-lastY)*0.007;\n  lastX=e.touches[0].clientX; lastY=e.touches[0].clientY;\n  drawPreview(); e.preventDefault();\n},{passive:false});\ncv.addEventListener('touchend',()=\u003e{ dragging=false; scheduleAuto(); });\n\nfunction stopAuto(){ autoRotating=false; clearTimeout(autoTimer); }\nfunction scheduleAuto(){ autoTimer=setTimeout(()=\u003e{ autoRotating=true; autoLoop(); },3000); }\nfunction autoLoop(){ if(!autoRotating||dragging)return; rotY+=0.004; drawPreview(); requestAnimationFrame(autoLoop); }\n\nfunction project([x,y,z]){\n  const cosX=Math.cos(rotX),sinX=Math.sin(rotX);\n  const cosY=Math.cos(rotY),sinY=Math.sin(rotY);\n  let y2=y*cosX-z*sinX, z2=y*sinX+z*cosX;\n  let x2=x*cosY+z2*sinY, z3=-x*sinY+z2*cosY;\n  const S=cv.width\/2;\n  return [S+x2*zoom*3.8, S-y2*zoom*3.8, z3];\n}\n\nfunction buildWordPoints(){\n  const cfg = SIZES[selectedSize];\n  const wordList = getWords();\n  const N = wordList.length;\n  words3D = [];\n  if(N===0) return;\n  for(let i=0;i\u003cN;i++){\n    const t = N===1?0.5:i\/(N-1);\n    const r = cfg.maxR-(cfg.maxR-cfg.minR)*t;\n    const cz = cfg.height\/2 - t*cfg.height;\n    const ang = -(t*cfg.turns*2*Math.PI);  \/\/ negative = clockwise from top = L→R reading order\n    words3D.push({\n      word: wordList[i],\n      pos: [Math.cos(ang)*r, cz, Math.sin(ang)*r],\n      t, ang\n    });\n  }\n}\n\nfunction drawPreview(){\n  const W=cv.width, H=cv.height;\n  ctx2.clearRect(0,0,W,H);\n\n  \/\/ Background glow\n  const grd = ctx2.createRadialGradient(W\/2,H\/2,0,W\/2,H\/2,W*0.6);\n  grd.addColorStop(0,'#0d1f3c'); grd.addColorStop(1,'#0a0b0f');\n  ctx2.fillStyle=grd; ctx2.fillRect(0,0,W,H);\n\n  const cfg = SIZES[selectedSize];\n  drawCrystalShell(cfg);\n\n  if(words3D.length===0){\n    ctx2.fillStyle='rgba(74,158,255,0.25)';\n    ctx2.font='14px Inter';\n    ctx2.textAlign='center';\n    ctx2.fillText('Enter your text to see the preview',W\/2,H\/2);\n    return;\n  }\n\n  \/\/ Draw spiral path\n  const sorted = [...words3D].sort((a,b)=\u003ea.ang-b.ang);\n  ctx2.beginPath();\n  sorted.forEach((p,i)=\u003e{\n    const [px,py]=project(p.pos);\n    i===0?ctx2.moveTo(px,py):ctx2.lineTo(px,py);\n  });\n  ctx2.strokeStyle='rgba(74,158,255,0.2)'; ctx2.lineWidth=1;\n  ctx2.setLineDash([3,5]); ctx2.stroke(); ctx2.setLineDash([]);\n\n  \/\/ Draw words back→front\n  const byDepth=[...words3D].map(p=\u003e({...p,proj:project(p.pos)}))\n    .sort((a,b)=\u003eb.proj[2]-a.proj[2]);\n\n  byDepth.forEach(p=\u003e{\n    const [px,py,pz]=p.proj;\n    const depth=Math.max(0,Math.min(1,(pz+80)\/160));\n    const alpha=0.25+depth*0.75;\n    const hue=200+p.t*50;\n    const size=Math.max(9,Math.round(11*zoom*(0.6+depth*0.5)));\n\n    \/\/ Glow dot\n    const grd2=ctx2.createRadialGradient(px,py,0,px,py,size*1.2);\n    grd2.addColorStop(0,`hsla(${hue},80%,70%,${alpha*0.4})`);\n    grd2.addColorStop(1,'transparent');\n    ctx2.fillStyle=grd2;\n    ctx2.beginPath(); ctx2.arc(px,py,size*1.2,0,Math.PI*2); ctx2.fill();\n\n    ctx2.font=`500 ${size}px Inter`;\n    ctx2.textAlign='center'; ctx2.textBaseline='middle';\n    ctx2.fillStyle=`hsla(${hue},80%,${55+depth*35}%,${alpha})`;\n    ctx2.fillText(p.word,px,py);\n  });\n}\n\nfunction drawCrystalShell(cfg){\n  const N=40;\n  \/\/ Draw horizontal rings at several heights\n  [0,0.2,0.4,0.6,0.8,1.0].forEach(tv=\u003e{\n    const r=cfg.maxR-(cfg.maxR-cfg.minR)*tv;\n    const cz=cfg.height\/2-tv*cfg.height;\n    ctx2.beginPath();\n    for(let i=0;i\u003c=N;i++){\n      const a=(i\/N)*Math.PI*2;\n      const [px,py]=project([Math.cos(a)*r,cz,Math.sin(a)*r]);\n      i===0?ctx2.moveTo(px,py):ctx2.lineTo(px,py);\n    }\n    ctx2.strokeStyle='rgba(74,158,255,0.08)'; ctx2.lineWidth=0.5; ctx2.stroke();\n  });\n  \/\/ Silhouette lines\n  [0,Math.PI\/2,Math.PI,Math.PI*1.5].forEach(ang=\u003e{\n    ctx2.beginPath();\n    for(let i=0;i\u003c=20;i++){\n      const tv=i\/20;\n      const r=cfg.maxR-(cfg.maxR-cfg.minR)*tv;\n      const cz=cfg.height\/2-tv*cfg.height;\n      const [px,py]=project([Math.cos(ang)*r,cz,Math.sin(ang)*r]);\n      i===0?ctx2.moveTo(px,py):ctx2.lineTo(px,py);\n    }\n    ctx2.strokeStyle='rgba(74,158,255,0.12)'; ctx2.lineWidth=0.7; ctx2.stroke();\n  });\n}\n\n\/\/ ── WORDS \u0026 STATS ─────────────────────────────────────────────────────────\nfunction getWords(){\n  return document.getElementById('lyrics').value\n    .split(\/[\\s\\n]+\/).map(w=\u003ew.replace(\/[.,!?'\"]\/g,'')).filter(Boolean);\n}\n\nfunction onLyricsChange(el){\n  const words=getWords();\n  const wc=document.getElementById('char-count');\n  wc.textContent=`${words.length} words`;\n  wc.className='char-count'+(words.length\u003e200?' warn':'');\n  document.getElementById('order-btn').disabled=words.length\u003c3;\n  clearTimeout(debounceTimer);\n  debounceTimer=setTimeout(()=\u003e{ buildWordPoints(); drawPreview(); updatePreviewStats(); },300);\n}\n\nfunction updatePreviewStats(){\n  const words=getWords();\n  const cfg=SIZES[selectedSize];\n  document.getElementById('stat-words').textContent=words.length;\n  document.getElementById('stat-size').textContent=cfg.label;\n  document.getElementById('stat-turns').textContent=words.length\u003e0?cfg.turns.toFixed(2):'—';\n}\n\n\/\/ ── SIZE SELECTION ────────────────────────────────────────────────────────\nfunction selectSize(el,size,price){\n  document.querySelectorAll('.size-card').forEach(c=\u003ec.classList.remove('active'));\n  el.classList.add('active');\n  selectedSize=size; selectedPrice=price;\n  buildWordPoints(); drawPreview(); updatePreviewStats();\n}\n\n\/\/ ── ORDER MODAL ───────────────────────────────────────────────────────────\nfunction openOrder(){\n  const words=getWords();\n  const cfg=SIZES[selectedSize];\n  const gift=document.getElementById('gift-msg').value.trim();\n  document.getElementById('sum-size').textContent=cfg.label;\n  document.getElementById('sum-words').textContent=words.length+' words';\n  document.getElementById('sum-gift').textContent=gift||'None';\n  document.getElementById('sum-price').textContent='$'+selectedPrice+' + shipping';\n  document.getElementById('order-status').className='status';\n  document.getElementById('modal-overlay').classList.add('open');\n}\nfunction closeOrder(){ document.getElementById('modal-overlay').classList.remove('open'); }\nfunction closeOrderIfOutside(e){ if(e.target===document.getElementById('modal-overlay'))closeOrder(); }\n\n\/\/ ── SUBMIT ORDER ──────────────────────────────────────────────────────────\nasync function submitOrder(){\n  const fname=document.getElementById('fname').value.trim();\n  const lname=document.getElementById('lname').value.trim();\n  const email=document.getElementById('email').value.trim();\n  const phone=document.getElementById('phone').value.trim();\n  const address=document.getElementById('address').value.trim();\n  const notes=document.getElementById('notes').value.trim();\n  const lyrics=document.getElementById('lyrics').value.trim();\n  const gift=document.getElementById('gift-msg').value.trim();\n  const cfg=SIZES[selectedSize];\n\n  if(!fname||!lname){return showStatus('error','Please enter your first and last name.');}\n  if(!email||!email.includes('@')){return showStatus('error','Please enter a valid email address.');}\n  if(!address){return showStatus('error','Please enter a shipping address.');}\n  if(!lyrics){return showStatus('error','Please enter your lyrics or message.');}\n\n  const btn=document.getElementById('submit-btn');\n  btn.innerHTML='\u003cspan class=\"spinner\"\u003e\u003c\/span\u003eSending…';\n  btn.disabled=true;\n\n  \/\/ Build the email body using mailto (no server needed for Option 2)\n  const subject=encodeURIComponent(`New Crystal Order — ${fname} ${lname} — ${cfg.label}`);\n  const body=encodeURIComponent(\n`NEW CRYSTAL TORNADO ORDER\n═══════════════════════════════════════\n\nCUSTOMER DETAILS\n  Name:     ${fname} ${lname}\n  Email:    ${email}\n  Phone:    ${phone||'—'}\n  Address:  ${address}\n\nORDER DETAILS\n  Crystal:  ${cfg.label}\n  Price:    $${selectedPrice} + shipping\n  Words:    ${getWords().length}\n\nLYRICS \/ MESSAGE\n───────────────────────────────────────\n${lyrics}\n───────────────────────────────────────\n\nGIFT MESSAGE\n${gift||'(none)'}\n\nSPECIAL INSTRUCTIONS\n${notes||'(none)'}\n\n═══════════════════════════════════════\nOrder submitted via Etches Memories Crystal Designer\n`\n  );\n\n  \/\/ Use mailto to open their email client — works without a server\n  const mailtoLink=`mailto:jasonetchesmemories@gmail.com?subject=${subject}\u0026body=${body}`;\n\n  \/\/ Also try to open mailto and show confirmation\n  setTimeout(()=\u003e{\n    window.location.href=mailtoLink;\n    btn.innerHTML='Send Order Request';\n    btn.disabled=false;\n    showStatus('success',\n      `Your order request is ready! Your email client should open automatically. ` +\n      `If it doesn\\'t, email us directly at jasonetchesmemories@gmail.com with your details. ` +\n      `We\\'ll confirm your design within 24 hours.`\n    );\n  },600);\n}\n\nfunction showStatus(type,msg){\n  const el=document.getElementById('order-status');\n  el.className='status '+type;\n  el.textContent=msg;\n}\n\n\/\/ ── INIT ──────────────────────────────────────────────────────────────────\nfunction resizeCanvas(){\n  const rect=cv.getBoundingClientRect();\n  cv.width=rect.width*window.devicePixelRatio||600;\n  cv.height=rect.width*window.devicePixelRatio||600;\n  drawPreview();\n}\nwindow.addEventListener('resize',resizeCanvas);\nresizeCanvas();\nupdatePreviewStats();\nautoLoop();\n\u003c\/script\u003e xt and more room for longer lyrics or poems.\u003c\/li\u003e\n\u003c\/ul\u003e\n\u003cp\u003e\u003cem\u003eNo payment is charged until we confirm your design. All crystals are made to order — no two are alike.\u003c\/em\u003e\u003c\/p\u003e\n\u003cp\u003e]]\u0026gt;\u003c\/p\u003e","brand":"Etches Memories","offers":[{"title":"Classic — 50×50×80mm","offer_id":49350039044324,"sku":"CRYSTAL-TORNADO-50x50x80","price":89.0,"currency_code":"USD","in_stock":true},{"title":"Grand — 70×70×100mm","offer_id":49350039077092,"sku":"CRYSTAL-TORNADO-70x70x100","price":129.0,"currency_code":"USD","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0804\/5887\/5108\/files\/IMG_2642.jpg?v=1783392972","url":"https:\/\/etchamemory.com\/products\/crystal-tornado-custom-lyric-text-engraving","provider":"Etch A Memory","version":"1.0","type":"link"}