1783 lines
93 KiB
HTML
1783 lines
93 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-theme="light">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>QR Master - Free Dynamic QR Code Generator</title>
|
|
<meta name="description" content="QR Master is a free dynamic QR code generator with tracking, editable destinations, custom branding, interactive ROI calculator, and bulk QR creation.">
|
|
<meta name="color-scheme" content="light dark">
|
|
<script>if (location.hash === "#dark") document.documentElement.setAttribute("data-theme","dark");</script>
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,500;0,6..72,600;1,6..72,500&family=Outfit:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
|
|
<style>
|
|
/* ============================================================
|
|
QR MASTER — flagship standalone build
|
|
Design read: freemium utility landing page (QR generator) for
|
|
small-business owners and marketers. Confident, print-proof
|
|
register: an off-black/off-white base with one saturated
|
|
signal-red accent, tied literally to the product (a halftone
|
|
dot shader stands in for what a printed QR code actually is).
|
|
Dials: VARIANCE 7, MOTION 6, DENSITY 3.
|
|
Shape lock: 6px radius everywhere. One accent used throughout.
|
|
============================================================ */
|
|
|
|
:root {
|
|
--font-display: "Newsreader", ui-serif, Georgia, serif;
|
|
--font-sans: "Outfit", ui-sans-serif, system-ui, sans-serif;
|
|
--font-mono: "IBM Plex Mono", ui-monospace, Consolas, monospace;
|
|
|
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
--dur-micro: 160ms;
|
|
--dur-ui: 320ms;
|
|
|
|
--r: 6px;
|
|
--r-sm: 4px;
|
|
--container: 1240px;
|
|
}
|
|
|
|
html[data-theme="light"] {
|
|
color-scheme: light;
|
|
--paper: #F4F5F1;
|
|
--paper-2: #EAEBE5;
|
|
--panel: #FFFFFF;
|
|
--ink: #131310;
|
|
--ink-2: #46453D;
|
|
--muted: #82806F;
|
|
--line: #DEDCD0;
|
|
--line-strong: #131310;
|
|
--accent: #DE3A16;
|
|
--accent-hover: #B92E10;
|
|
--accent-text: #B92E10;
|
|
--accent-soft: rgba(222, 58, 22, 0.08);
|
|
--mark-band: rgba(222, 58, 22, 0.22);
|
|
--danger: #DE3A16;
|
|
--success: #1E8A4C;
|
|
--cta: #131310;
|
|
--cta-hover: #000000;
|
|
--deep: #131310;
|
|
--deep-text: #F4F5F1;
|
|
--deep-muted: #9E9C8C;
|
|
--deep-accent: #FF6A3D;
|
|
--shadow-ink: rgba(19, 19, 16, 0.9);
|
|
--hard-shadow: 4px 4px 0 var(--shadow-ink);
|
|
--nav-bg: rgba(244, 245, 241, 0.88);
|
|
--dots: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Ccircle cx='1.4' cy='1.4' r='1.2' fill='%23131310' fill-opacity='0.12'/%3E%3C/svg%3E");
|
|
}
|
|
|
|
html[data-theme="dark"] {
|
|
color-scheme: dark;
|
|
--paper: #101010;
|
|
--paper-2: #1A1A17;
|
|
--panel: #18180F;
|
|
--ink: #F2F2EC;
|
|
--ink-2: #B7B5A6;
|
|
--muted: #767566;
|
|
--line: #2E2E27;
|
|
--line-strong: #F2F2EC;
|
|
--accent: #FF6A3D;
|
|
--accent-hover: #FF8560;
|
|
--accent-text: #FF8560;
|
|
--accent-soft: rgba(255, 106, 61, 0.14);
|
|
--mark-band: rgba(255, 106, 61, 0.28);
|
|
--danger: #FF6A3D;
|
|
--success: #37B96C;
|
|
--cta: #F2F2EC;
|
|
--cta-hover: #FFFFFF;
|
|
--deep: #050504;
|
|
--deep-text: #F2F2EC;
|
|
--deep-muted: #918F7E;
|
|
--deep-accent: #FF8560;
|
|
--shadow-ink: rgba(0, 0, 0, 0.75);
|
|
--hard-shadow: 4px 4px 0 var(--shadow-ink);
|
|
--nav-bg: rgba(16, 16, 16, 0.88);
|
|
--dots: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Ccircle cx='1.4' cy='1.4' r='1.2' fill='%23F2F2EC' fill-opacity='0.1'/%3E%3C/svg%3E");
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
html { scroll-behavior: smooth; }
|
|
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
|
|
|
|
body {
|
|
font-family: var(--font-sans);
|
|
background: var(--paper);
|
|
color: var(--ink);
|
|
line-height: 1.55;
|
|
-webkit-font-smoothing: antialiased;
|
|
text-rendering: optimizeLegibility;
|
|
overflow-x: hidden;
|
|
transition: background-color var(--dur-ui) var(--ease-out), color var(--dur-ui) var(--ease-out);
|
|
}
|
|
|
|
::selection { background: var(--accent); color: #ffffff; }
|
|
::-webkit-scrollbar { width: 9px; }
|
|
::-webkit-scrollbar-track { background: var(--paper); }
|
|
::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 0; }
|
|
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
|
|
|
|
a { color: inherit; text-decoration: none; }
|
|
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible {
|
|
outline: 2px solid var(--accent); outline-offset: 2px;
|
|
}
|
|
|
|
.skip-link {
|
|
position: absolute; top: -100px; left: 12px; z-index: 999;
|
|
background: var(--accent); color: #ffffff; font-weight: 600; font-size: 0.875rem;
|
|
padding: 12px 18px; border-radius: var(--r-sm);
|
|
transition: top var(--dur-micro) var(--ease-out);
|
|
}
|
|
.skip-link:focus { top: 12px; }
|
|
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
|
|
img, svg, canvas { display: block; max-width: 100%; }
|
|
ul { list-style: none; }
|
|
|
|
.container { max-width: var(--container); margin: 0 auto; padding: 0 24px; }
|
|
h1, h2, h3, h4 { font-family: var(--font-display); letter-spacing: -0.015em; text-wrap: balance; font-weight: 600; }
|
|
|
|
.section { padding: 108px 0; position: relative; }
|
|
.section--tight { padding: 76px 0; }
|
|
|
|
.eyebrow {
|
|
display: inline-flex; align-items: center; gap: 8px;
|
|
font-family: var(--font-mono); font-size: 11px; font-weight: 600;
|
|
letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-text);
|
|
}
|
|
|
|
.section-head { max-width: 640px; margin-bottom: 48px; }
|
|
.section-head h2 { font-size: clamp(1.9rem, 3.6vw, 2.7rem); line-height: 1.08; margin: 12px 0 14px; }
|
|
.section-head p { color: var(--ink-2); font-size: 1.0625rem; line-height: 1.6; max-width: 56ch; }
|
|
|
|
mark {
|
|
background: none; color: var(--accent-text); font-style: italic; font-weight: 600;
|
|
box-shadow: inset 0 -0.14em 0 var(--mark-band);
|
|
-webkit-box-decoration-break: clone; box-decoration-break: clone;
|
|
}
|
|
em.italic-safe { font-style: italic; line-height: 1.15; display: inline-block; padding-bottom: 0.08em; }
|
|
|
|
.btn {
|
|
display: inline-flex; align-items: center; justify-content: center; gap: 10px;
|
|
font-weight: 600; font-size: 0.95rem; border-radius: var(--r); letter-spacing: -0.005em;
|
|
padding: 13px 24px; white-space: nowrap; border: 1px solid transparent;
|
|
transition: transform var(--dur-micro) var(--ease-out), box-shadow var(--dur-micro) var(--ease-out),
|
|
background var(--dur-micro) var(--ease-out), border-color var(--dur-micro) var(--ease-out);
|
|
}
|
|
.btn:active { transform: translate(1px, 1px); box-shadow: none; }
|
|
.btn-primary { background: var(--cta); color: var(--paper); }
|
|
.btn-primary:hover { background: var(--cta-hover); transform: translate(-2px, -2px); box-shadow: var(--hard-shadow); }
|
|
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line-strong); }
|
|
.btn-ghost:hover { border-color: var(--accent); color: var(--accent-text); transform: translate(-2px, -2px); box-shadow: 4px 4px 0 var(--line); }
|
|
.btn-sm { padding: 9px 16px; font-size: 0.875rem; }
|
|
|
|
.panel {
|
|
background: var(--panel); border: 1px solid var(--line); border-radius: var(--r);
|
|
transition: border-color var(--dur-ui) var(--ease-out), transform var(--dur-ui) var(--ease-out), box-shadow var(--dur-ui) var(--ease-out);
|
|
}
|
|
.panel-hover:hover { border-color: var(--ink); transform: translate(-3px, -3px); box-shadow: var(--hard-shadow); }
|
|
|
|
/* Scroll progress (GSAP-driven, no scroll listeners) */
|
|
.progress { position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 300; pointer-events: none; }
|
|
.progress span { display: block; height: 100%; width: 100%; background: var(--accent); transform-origin: 0 50%; transform: scaleX(0); }
|
|
|
|
/* Navigation */
|
|
.nav {
|
|
position: fixed; top: 0; left: 0; right: 0; z-index: 200; height: 72px;
|
|
background: var(--nav-bg); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
|
|
border-bottom: 1px solid transparent;
|
|
transition: border-color var(--dur-ui) var(--ease-out);
|
|
}
|
|
.nav.scrolled { border-bottom-color: var(--line); }
|
|
.nav-inner {
|
|
max-width: var(--container); margin: 0 auto; padding: 0 24px; height: 72px;
|
|
display: flex; align-items: center; justify-content: space-between; gap: 24px;
|
|
}
|
|
.logo { display: inline-flex; align-items: center; gap: 10px; font-family: var(--font-display); font-weight: 600; font-size: 1.1875rem; letter-spacing: -0.01em; }
|
|
.logo-mark {
|
|
width: 32px; height: 32px; background: var(--cta); color: var(--paper);
|
|
display: grid; place-items: center; border-radius: var(--r-sm);
|
|
}
|
|
.nav-links { display: flex; align-items: center; gap: 4px; }
|
|
.nav-links a {
|
|
font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
|
|
color: var(--ink-2); padding: 8px 12px; border-radius: var(--r-sm);
|
|
transition: color var(--dur-micro) var(--ease-out), background var(--dur-micro) var(--ease-out);
|
|
}
|
|
.nav-links a:hover { color: var(--ink); background: var(--paper-2); }
|
|
.nav-links a.active { color: var(--accent-text); background: var(--accent-soft); }
|
|
.nav-actions { display: flex; align-items: center; gap: 10px; }
|
|
|
|
.nav-burger {
|
|
display: none; width: 38px; height: 38px; border-radius: var(--r-sm);
|
|
border: 1px solid var(--line-strong); align-items: center; justify-content: center;
|
|
color: var(--ink); background: transparent; flex-direction: column; gap: 4px;
|
|
}
|
|
.nav-burger span { display: block; width: 16px; height: 2px; background: currentColor; transition: transform var(--dur-micro) var(--ease-out), opacity var(--dur-micro) var(--ease-out); }
|
|
.nav-burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
|
|
.nav-burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
|
|
.nav-burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
|
|
|
|
.nav-mobile-panel {
|
|
display: none; position: fixed; top: 72px; left: 0; right: 0; z-index: 199;
|
|
background: var(--panel); border-bottom: 1px solid var(--line);
|
|
padding: 8px 24px 20px; flex-direction: column; gap: 2px;
|
|
}
|
|
.nav-mobile-panel.open { display: flex; }
|
|
.nav-mobile-panel a {
|
|
font-family: var(--font-mono); font-size: 13px; font-weight: 600; letter-spacing: 0.03em; text-transform: uppercase;
|
|
color: var(--ink-2); padding: 14px 4px; border-bottom: 1px solid var(--line);
|
|
}
|
|
.nav-mobile-panel a:last-of-type { border-bottom: none; }
|
|
|
|
.theme-toggle {
|
|
width: 38px; height: 38px; border-radius: var(--r-sm); border: 1px solid var(--line-strong);
|
|
display: grid; place-items: center; color: var(--ink-2); background: transparent;
|
|
transition: color var(--dur-micro) var(--ease-out), border-color var(--dur-micro) var(--ease-out);
|
|
}
|
|
.theme-toggle:hover { color: var(--accent-text); border-color: var(--accent); }
|
|
.theme-toggle .icon-sun { display: none; }
|
|
html[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
|
|
html[data-theme="dark"] .theme-toggle .icon-moon { display: none; }
|
|
|
|
/* ==================== HERO — asymmetric split ====================
|
|
Halftone canvas is the literal print-proof metaphor for a QR code;
|
|
the right column is the actual live product output, not a mockup. */
|
|
.hero { position: relative; overflow: hidden; padding: 152px 0 0; }
|
|
.hero-halftone { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; opacity: 0.5; }
|
|
.hero-fade {
|
|
position: absolute; inset: 0; z-index: 1; pointer-events: none;
|
|
background: linear-gradient(to bottom, var(--paper) 0%, transparent 20%, transparent 82%, var(--paper) 100%);
|
|
}
|
|
.hero-grid {
|
|
position: relative; z-index: 2;
|
|
display: grid; grid-template-columns: 1.05fr 0.95fr; gap: 56px; align-items: center;
|
|
padding-bottom: 84px;
|
|
}
|
|
.hero-copy .eyebrow { margin-bottom: 4px; }
|
|
.hero h1 { font-size: clamp(2.5rem, 4.8vw, 4rem); line-height: 1.04; margin: 16px 0 18px; }
|
|
.hero-sub { color: var(--ink-2); font-size: 1.1rem; line-height: 1.55; max-width: 46ch; }
|
|
.hero-ctas { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 28px; }
|
|
|
|
.hero-asset { position: relative; justify-self: center; width: 100%; max-width: 400px; }
|
|
.qr-frame {
|
|
position: relative; background: var(--panel); border: 1px solid var(--line-strong);
|
|
border-radius: var(--r); padding: 26px; box-shadow: var(--hard-shadow);
|
|
transition: transform var(--dur-ui) var(--ease-out), box-shadow var(--dur-ui) var(--ease-out);
|
|
}
|
|
.qr-frame:hover { transform: translateY(-2px); box-shadow: 7px 7px 0 var(--shadow-ink); }
|
|
.qr-frame canvas { width: 100%; height: auto; display: block; border-radius: 3px; }
|
|
.reg { position: absolute; width: 13px; height: 13px; color: var(--muted); }
|
|
.reg::before, .reg::after { content: ""; position: absolute; background: currentColor; }
|
|
.reg::before { left: 50%; top: 0; width: 1px; height: 100%; }
|
|
.reg::after { top: 50%; left: 0; width: 100%; height: 1px; }
|
|
.reg-tl { top: -19px; left: -19px; } .reg-tr { top: -19px; right: -19px; }
|
|
.reg-bl { bottom: -19px; left: -19px; } .reg-br { bottom: -19px; right: -19px; }
|
|
.qr-meta { margin-top: 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
.qr-meta .live { font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--accent-text); display: inline-flex; align-items: center; gap: 6px; }
|
|
.qr-meta .live::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: blink 1.4s infinite; }
|
|
@keyframes blink { to { visibility: hidden; } }
|
|
.qr-echo { margin-top: 10px; font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); word-break: break-all; }
|
|
|
|
.hero-types { margin-top: 16px; display: flex; flex-wrap: wrap; gap: 8px; }
|
|
.type-chip {
|
|
font-family: var(--font-mono); font-size: 10px; font-weight: 600; letter-spacing: 0.03em;
|
|
color: var(--ink-2); border: 1px solid var(--line); border-radius: var(--r-sm);
|
|
padding: 5px 10px; background: var(--panel); cursor: pointer;
|
|
transition: border-color var(--dur-micro) var(--ease-out), color var(--dur-micro) var(--ease-out), background var(--dur-micro), transform var(--dur-micro);
|
|
}
|
|
.type-chip:hover { transform: translateY(-1px); border-color: var(--accent); }
|
|
.type-chip.active { border-color: var(--accent); color: var(--accent-text); background: var(--accent-soft); }
|
|
|
|
/* Stats strip */
|
|
.stats { border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--paper-2); position: relative; z-index: 2; }
|
|
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); }
|
|
.stat { padding: 30px 24px; border-left: 1px solid var(--line); }
|
|
.stat:first-child { border-left: none; }
|
|
.stat-num { font-family: var(--font-display); font-size: clamp(1.7rem, 2.6vw, 2.2rem); font-weight: 600; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; line-height: 1; }
|
|
.stat-num em { font-style: normal; color: var(--accent-text); }
|
|
.stat-label { margin-top: 8px; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
|
|
|
|
/* Generator Studio */
|
|
.gen-wrap { display: grid; grid-template-columns: 1.05fr 0.95fr; gap: 28px; align-items: stretch; }
|
|
.gen-panel { padding: 32px; display: flex; flex-direction: column; gap: 18px; }
|
|
.field label { display: block; font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-2); margin-bottom: 8px; }
|
|
.input, .select {
|
|
width: 100%; padding: 12px 14px; border-radius: var(--r-sm);
|
|
background: var(--paper); border: 1px solid var(--line-strong); color: var(--ink);
|
|
font: inherit; font-size: 0.9375rem;
|
|
transition: border-color var(--dur-micro) var(--ease-out);
|
|
}
|
|
.input:focus, .select:focus { outline: none; border-color: var(--accent); }
|
|
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
|
|
.swatch-group { display: flex; gap: 8px; align-items: center; margin-top: 6px; flex-wrap: wrap; }
|
|
.swatch-btn { width: 28px; height: 28px; border-radius: 50%; border: 2px solid var(--line-strong); cursor: pointer; transition: transform var(--dur-micro), border-color var(--dur-micro), box-shadow var(--dur-micro); position: relative; }
|
|
.swatch-btn:hover { transform: scale(1.15); border-color: var(--accent); }
|
|
.swatch-btn.active { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
|
|
.color-row { display: flex; gap: 10px; align-items: stretch; }
|
|
.color-row input[type="color"] { width: 44px; height: 44px; padding: 2px; border-radius: var(--r-sm); background: var(--paper); border: 1px solid var(--line-strong); cursor: pointer; flex-shrink: 0; }
|
|
.range { width: 100%; accent-color: var(--accent); height: 30px; cursor: pointer; }
|
|
.range-val { font-family: var(--font-mono); font-size: 11px; color: var(--muted); text-align: center; }
|
|
|
|
.contrast-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; border-top: 1px dashed var(--line-strong); padding-top: 16px; }
|
|
.contrast-badge { font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; padding: 5px 10px; border-radius: var(--r-sm); }
|
|
.contrast-badge.ok { background: rgba(30, 138, 76, 0.12); color: var(--success); border: 1px solid var(--success); }
|
|
.contrast-badge.bad { background: rgba(222, 58, 22, 0.12); color: var(--danger); border: 1px solid var(--danger); }
|
|
.contrast-val { font-family: var(--font-mono); font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
|
|
.gen-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
.gen-actions .btn { padding: 12px 14px; font-size: 0.875rem; }
|
|
|
|
.gen-preview {
|
|
position: relative; border-radius: var(--r); overflow: hidden;
|
|
border: 1px solid var(--line); background: var(--paper-2);
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
padding: 36px 24px; gap: 18px; min-height: 480px;
|
|
}
|
|
.gen-preview::before { content: ""; position: absolute; inset: 0; pointer-events: none; background-image: var(--dots); background-size: 26px 26px; }
|
|
.qr-stage { position: relative; z-index: 2; padding: 18px; background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--r); box-shadow: var(--hard-shadow); }
|
|
.qr-stage .corner { position: absolute; width: 12px; height: 12px; border: 2px solid var(--accent); pointer-events: none; }
|
|
.qr-stage .corner.tl { top: -6px; left: -6px; border-right: 0; border-bottom: 0; }
|
|
.qr-stage .corner.tr { top: -6px; right: -6px; border-left: 0; border-bottom: 0; }
|
|
.qr-stage .corner.bl { bottom: -6px; left: -6px; border-right: 0; border-top: 0; }
|
|
.qr-stage .corner.br { bottom: -6px; right: -6px; border-left: 0; border-top: 0; }
|
|
.qr-url { position: relative; z-index: 2; max-width: 100%; font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-2); background: var(--panel); border: 1px solid var(--line); padding: 8px 16px; border-radius: var(--r-sm); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.qr-note { position: relative; z-index: 2; font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
|
|
|
|
/* Bento feature grid — four cells for four real pieces of content */
|
|
.bento { display: grid; grid-template-columns: 1.4fr 1fr; grid-template-rows: auto auto; gap: 14px; }
|
|
.bento-cell { padding: 30px; border-radius: var(--r); position: relative; }
|
|
.bento-hero { grid-row: 1 / 3; background: var(--accent-soft); border: 1px solid var(--line); display: flex; flex-direction: column; justify-content: flex-end; gap: 10px; min-height: 320px; }
|
|
.bento-hero h3 { font-size: 1.5rem; line-height: 1.15; max-width: 26ch; }
|
|
.bento-hero p { color: var(--ink-2); font-size: 0.9375rem; max-width: 38ch; }
|
|
.bento-stat { background: var(--panel); border: 1px solid var(--line); display: flex; flex-direction: column; justify-content: center; gap: 6px; }
|
|
.bento-stat .num { font-family: var(--font-display); font-size: 2.6rem; font-weight: 600; line-height: 1; letter-spacing: -0.02em; }
|
|
.bento-stat .lbl { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
|
|
.bento-cta { background: var(--cta); color: var(--paper); display: flex; flex-direction: column; justify-content: center; gap: 10px; }
|
|
.bento-cta h3 { color: var(--paper); font-size: 1.15rem; }
|
|
.bento-cta .btn-ghost { border-color: rgba(244,245,241,0.3); color: var(--paper); }
|
|
.bento-cta .btn-ghost:hover { border-color: var(--deep-accent); color: var(--deep-accent); box-shadow: 4px 4px 0 rgba(244,245,241,0.15); }
|
|
@media (max-width: 900px) { .bento { grid-template-columns: 1fr; } .bento-hero { grid-row: auto; min-height: 0; } }
|
|
|
|
/* Tools index with search filter */
|
|
.tools-filter { margin-bottom: 22px; }
|
|
.tools-filter .input { max-width: 420px; }
|
|
.tools-index { border-top: 1px solid var(--line-strong); }
|
|
.tool-row { display: grid; grid-template-columns: 44px 1fr 190px 30px; align-items: center; gap: 18px; padding: 15px 8px; border-bottom: 1px solid var(--line); transition: background var(--dur-micro) var(--ease-out), transform var(--dur-micro) var(--ease-out); }
|
|
.tool-row:hover { background: var(--panel); transform: translateX(6px); box-shadow: inset 3px 0 0 var(--accent); }
|
|
.tool-row .idx { font-family: var(--font-mono); font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
.tool-row h3 { font-family: var(--font-sans); font-weight: 600; font-size: 1rem; letter-spacing: -0.01em; }
|
|
.tool-row p { font-size: 0.875rem; color: var(--ink-2); }
|
|
.tool-row .arr { color: var(--muted); justify-self: end; transition: color var(--dur-micro) var(--ease-out), transform var(--dur-micro) var(--ease-out); }
|
|
.tool-row:hover .arr { color: var(--accent-text); transform: translate(2px, -2px); }
|
|
|
|
/* Link cards (merged workflows + use-cases, one grid, one family) */
|
|
.link-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
|
|
.link-card { padding: 22px 20px; position: relative; display: block; }
|
|
.link-card h3 { font-family: var(--font-sans); font-size: 1rem; font-weight: 600; line-height: 1.3; margin-bottom: 8px; letter-spacing: -0.01em; }
|
|
.link-card p { font-size: 0.8125rem; color: var(--ink-2); line-height: 1.55; }
|
|
.link-card .go { position: absolute; top: 18px; right: 18px; color: var(--muted); transition: color var(--dur-micro) var(--ease-out), transform var(--dur-micro) var(--ease-out); }
|
|
.link-card:hover .go { color: var(--accent-text); transform: translate(2px, -2px); }
|
|
.center-cta { text-align: center; margin-top: 36px; }
|
|
|
|
.alt-bg { background: var(--paper-2); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
|
|
|
/* Testimonials marquee — the one marquee on the page */
|
|
.testi { overflow: hidden; }
|
|
.marquee-mask { overflow: hidden; position: relative; mask-image: linear-gradient(90deg, transparent, black 5%, black 95%, transparent); -webkit-mask-image: linear-gradient(90deg, transparent, black 5%, black 95%, transparent); }
|
|
.marquee-track { display: flex; gap: 16px; width: max-content; padding: 8px 0 16px; }
|
|
.testi-card { width: 360px; flex-shrink: 0; padding: 22px; display: flex; flex-direction: column; gap: 12px; }
|
|
.testi-top { display: flex; align-items: center; justify-content: space-between; }
|
|
.stars { display: flex; gap: 2px; color: var(--accent-text); font-size: 13px; letter-spacing: 2px; font-family: var(--font-mono); }
|
|
.stars .off { color: var(--line-strong); }
|
|
.testi-card h3 { font-family: var(--font-sans); font-size: 0.95rem; font-weight: 600; letter-spacing: -0.01em; }
|
|
.testi-card > p { font-size: 0.85rem; color: var(--ink-2); line-height: 1.55; flex-grow: 1; max-height: 4.7em; overflow: hidden; }
|
|
.testi-author { border-top: 1px solid var(--line); padding-top: 12px; font-family: var(--font-mono); }
|
|
.testi-author b { display: block; font-size: 11.5px; letter-spacing: 0.01em; font-family: var(--font-sans); font-weight: 600; }
|
|
.testi-author span { display: block; font-size: 10px; color: var(--muted); letter-spacing: 0.05em; text-transform: uppercase; margin-top: 2px; }
|
|
|
|
/* Comparison — grouped rows, one border top/bottom, no per-row hairline noise */
|
|
.cmp { max-width: 880px; margin: 0 auto; }
|
|
.cmp-row { display: grid; grid-template-columns: 1.6fr 1fr 1fr; align-items: center; padding: 20px 8px; }
|
|
.cmp-head { border-bottom: 2px solid var(--line-strong); padding-bottom: 16px; margin-bottom: 8px; }
|
|
.cmp-head .ttl { font-family: var(--font-display); font-size: 1.15rem; font-weight: 600; }
|
|
.cmp-head .sub { font-family: var(--font-mono); font-size: 10px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); margin-top: 3px; }
|
|
.cmp-head .dyn .ttl { color: var(--accent-text); }
|
|
.cmp .cmp-row:not(.cmp-head) + .cmp-row:not(.cmp-head) { border-top: 1px solid var(--line); }
|
|
.cmp-cap { font-weight: 600; font-size: 0.9375rem; }
|
|
.cmp-cap span.note { display: block; font-size: 0.75rem; color: var(--muted); font-weight: 400; margin-top: 2px; }
|
|
.cmp-yes, .cmp-no { font-family: var(--font-mono); font-size: 12.5px; font-weight: 700; display: inline-flex; align-items: center; gap: 7px; }
|
|
.cmp-yes { color: var(--accent-text); }
|
|
.cmp-no { color: var(--muted); }
|
|
.cmp .cmp-row:last-of-type { padding-bottom: 0; }
|
|
.cmp-foot { border-top: 2px solid var(--line-strong); padding-top: 16px; margin-top: 8px; }
|
|
|
|
/* Interactive ROI Calculator */
|
|
.roi-card { border: 1px solid var(--line-strong); border-radius: var(--r); background: var(--panel); display: grid; grid-template-columns: 1.2fr 0.8fr; overflow: hidden; box-shadow: var(--hard-shadow); }
|
|
.roi-left { padding: 44px 40px; display: flex; flex-direction: column; gap: 18px; }
|
|
.roi-left h2 { font-size: clamp(1.6rem, 2.8vw, 2.2rem); line-height: 1.08; }
|
|
.roi-right { border-left: 1px solid var(--line); background: var(--deep); color: var(--deep-text); display: flex; flex-direction: column; justify-content: center; padding: 44px 34px; gap: 10px; }
|
|
.roi-right .big-savings { font-family: var(--font-display); font-size: clamp(2.3rem, 3.6vw, 3.3rem); font-weight: 600; letter-spacing: -0.02em; color: var(--deep-accent); font-variant-numeric: tabular-nums; line-height: 1; }
|
|
.roi-right .lbl { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--deep-muted); }
|
|
|
|
/* Pricing */
|
|
.billing-toggle { display: inline-flex; align-items: center; gap: 14px; margin: 0 auto 44px; font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
|
|
.billing-toggle .opt.active { color: var(--ink); }
|
|
.switch { width: 50px; height: 27px; border-radius: 999px; background: var(--paper-2); border: 1px solid var(--line-strong); position: relative; cursor: pointer; transition: border-color var(--dur-micro) var(--ease-out); }
|
|
.switch:hover { border-color: var(--accent); }
|
|
.switch::after { content: ""; position: absolute; top: 3px; left: 3px; width: 19px; height: 19px; border-radius: 50%; background: var(--accent); transition: transform var(--dur-micro) var(--ease-out); }
|
|
.switch[aria-checked="true"]::after { transform: translateX(23px); }
|
|
.save-tag { font-family: var(--font-mono); font-size: 10px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-text); background: var(--accent-soft); padding: 3px 8px; border-radius: var(--r-sm); opacity: 0; transform: translateY(3px); transition: opacity var(--dur-micro), transform var(--dur-micro); }
|
|
.save-tag.show { opacity: 1; transform: none; }
|
|
|
|
.price-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; align-items: stretch; }
|
|
.price-card { padding: 28px 22px; display: flex; flex-direction: column; position: relative; }
|
|
.price-card.popular { background: var(--deep); color: var(--deep-text); border-color: var(--deep); transform: translate(-3px, -3px); box-shadow: 6px 6px 0 var(--accent); }
|
|
.price-card.popular:hover { transform: translate(-5px, -5px); box-shadow: 8px 8px 0 var(--accent); }
|
|
.pop-tag { position: absolute; top: -12px; left: 20px; background: var(--accent); color: #ffffff; font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; padding: 4px 10px; border-radius: var(--r-sm); }
|
|
.price-name { font-family: var(--font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
|
|
.price-card.popular .price-name { color: var(--deep-muted); }
|
|
.price-row { display: flex; align-items: baseline; gap: 6px; margin: 14px 0 4px; min-height: 52px; }
|
|
.price-val { font-family: var(--font-display); font-size: 2.6rem; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; line-height: 1; }
|
|
.price-per { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
|
|
.price-card.popular .price-per { color: var(--deep-muted); }
|
|
.price-feats { margin: 18px 0 22px; display: grid; gap: 9px; flex-grow: 1; }
|
|
.price-feats li { display: flex; align-items: flex-start; gap: 8px; font-size: 0.85rem; color: var(--ink-2); }
|
|
.price-card.popular .price-feats li { color: rgba(244, 245, 241, 0.82); }
|
|
.price-feats li::before { content: "+"; font-family: var(--font-mono); color: var(--accent-text); font-weight: 700; flex-shrink: 0; }
|
|
.price-card.popular .price-feats li::before { color: var(--deep-accent); }
|
|
.price-card .btn { width: 100%; }
|
|
.price-card.popular .btn-primary { background: var(--accent); color: #ffffff; }
|
|
.price-card.popular .btn-primary:hover { background: var(--accent-hover); }
|
|
|
|
/* FAQ */
|
|
.faq-list { max-width: 760px; margin: 0 auto; border-top: 1px solid var(--line-strong); }
|
|
.faq-item { border-bottom: 1px solid var(--line); }
|
|
.faq-q { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 20px 4px; text-align: left; background: transparent; font-family: var(--font-sans); font-size: 1.0625rem; font-weight: 600; letter-spacing: -0.01em; transition: color var(--dur-micro) var(--ease-out); }
|
|
.faq-q:hover { color: var(--accent-text); }
|
|
.faq-q .pm { font-family: var(--font-mono); font-size: 18px; font-weight: 500; color: var(--muted); transition: transform var(--dur-ui) var(--ease-out), color var(--dur-micro); flex-shrink: 0; }
|
|
.faq-item.open .faq-q .pm { transform: rotate(45deg); color: var(--accent-text); }
|
|
.faq-a { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--dur-ui) var(--ease-out); }
|
|
.faq-item.open .faq-a { grid-template-rows: 1fr; }
|
|
.faq-a > div { overflow: hidden; }
|
|
.faq-a p { padding: 0 4px 22px; color: var(--ink-2); font-size: 0.9375rem; line-height: 1.6; max-width: 68ch; }
|
|
|
|
/* Final CTA */
|
|
.final-cta { text-align: center; background: var(--deep); color: var(--deep-text); }
|
|
.final-cta h2 { font-size: clamp(2rem, 4.4vw, 3.4rem); line-height: 1.05; margin: 16px 0 16px; }
|
|
.final-cta p { color: var(--deep-muted); font-size: 1.0625rem; max-width: 44ch; margin: 0 auto 32px; }
|
|
|
|
/* Footer */
|
|
.footer { border-top: 1px solid var(--line-strong); background: var(--paper-2); padding: 56px 0 32px; }
|
|
.footer-grid { display: grid; grid-template-columns: 1.6fr 1fr 1fr 1fr 1fr; gap: 32px; margin-bottom: 40px; }
|
|
.footer-brand p { margin-top: 14px; font-size: 0.85rem; color: var(--muted); max-width: 30ch; }
|
|
.footer h4 { font-family: var(--font-mono); font-size: 10.5px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); margin-bottom: 14px; }
|
|
.footer li { margin-bottom: 9px; }
|
|
.footer-col a { font-size: 0.85rem; color: var(--ink-2); transition: color var(--dur-micro) var(--ease-out); }
|
|
.footer-col a:hover { color: var(--accent-text); }
|
|
.footer-bottom { border-top: 1px solid var(--line); padding-top: 20px; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.06em; color: var(--muted); }
|
|
|
|
/* Responsive */
|
|
@media (max-width: 1080px) {
|
|
.link-grid { grid-template-columns: repeat(2, 1fr); }
|
|
.price-grid { grid-template-columns: repeat(2, 1fr); }
|
|
.footer-grid { grid-template-columns: 1fr 1fr 1fr; }
|
|
}
|
|
@media (max-width: 900px) {
|
|
.section { padding: 72px 0; }
|
|
.hero { padding: 108px 0 0; }
|
|
.hero-grid { grid-template-columns: 1fr; gap: 40px; padding-bottom: 56px; }
|
|
.hero-asset { order: 2; }
|
|
.gen-wrap, .roi-card, .cmp-row { grid-template-columns: 1fr; }
|
|
.cmp-row { gap: 8px; }
|
|
.roi-right { border-left: none; border-top: 1px solid var(--line); }
|
|
.roi-left { padding: 32px 22px; }
|
|
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
.stat:nth-child(3) { border-left: none; }
|
|
.nav-links { display: none; }
|
|
.nav-burger { display: inline-flex; }
|
|
.tool-row { grid-template-columns: 36px 1fr 26px; }
|
|
.tool-row p { display: none; }
|
|
}
|
|
@media (max-width: 600px) {
|
|
.link-grid, .price-grid { grid-template-columns: 1fr; }
|
|
.testi-card { width: 290px; }
|
|
.gen-actions { grid-template-columns: 1fr; }
|
|
.field-row { grid-template-columns: 1fr; }
|
|
.footer-grid { grid-template-columns: 1fr 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<a class="skip-link" href="#top">Skip to content</a>
|
|
<div class="progress" aria-hidden="true"><span id="progressBar"></span></div>
|
|
|
|
<!-- ==================== NAVIGATION ==================== -->
|
|
<nav class="nav" id="nav">
|
|
<div class="nav-inner">
|
|
<a class="logo" href="#top" data-scroll>
|
|
<span class="logo-mark" aria-hidden="true">
|
|
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
|
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
|
|
<rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 14h3v3h-3zM21 14v.01M14 21v.01M21 21v.01M18 18h3"/>
|
|
</svg>
|
|
</span>
|
|
QR Master
|
|
</a>
|
|
<div class="nav-links" id="navLinks">
|
|
<a href="#generator" data-scroll data-nav="generator">Generator</a>
|
|
<a href="#tools" data-scroll data-nav="tools">Tools</a>
|
|
<a href="#pricing" data-scroll data-nav="pricing">Pricing</a>
|
|
<a href="#faq" data-scroll data-nav="faq">FAQ</a>
|
|
</div>
|
|
<div class="nav-actions">
|
|
<button class="theme-toggle" id="themeToggle" aria-label="Toggle color theme">
|
|
<svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
<svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32 1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>
|
|
</button>
|
|
<a class="btn btn-primary btn-sm" href="https://www.qrmaster.net/signup">Sign up free</a>
|
|
<button class="nav-burger" id="navBurger" aria-expanded="false" aria-controls="navMobilePanel" aria-label="Toggle navigation menu">
|
|
<span></span><span></span><span></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="nav-mobile-panel" id="navMobilePanel">
|
|
<a href="#generator" data-scroll data-mobile-nav>Generator</a>
|
|
<a href="#tools" data-scroll data-mobile-nav>Tools</a>
|
|
<a href="#pricing" data-scroll data-mobile-nav>Pricing</a>
|
|
<a href="#faq" data-scroll data-mobile-nav>FAQ</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- ==================== HERO ==================== -->
|
|
<header class="hero" id="top">
|
|
<canvas class="hero-halftone" id="heroHalftone" aria-hidden="true"></canvas>
|
|
<div class="hero-fade" aria-hidden="true"></div>
|
|
<div class="container hero-grid">
|
|
<div class="hero-copy">
|
|
<span class="eyebrow" data-hero="0">Free QR code generator</span>
|
|
<h1 data-hero="1">Codes that keep working after you print them.</h1>
|
|
<p class="hero-sub" data-hero="2">Create a QR code in seconds, then edit where it points anytime. No account needed to start.</p>
|
|
<div class="hero-ctas" data-hero="3">
|
|
<a class="btn btn-primary" href="#generator" data-scroll data-magnetic>Create a QR code
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14m-7-7 7 7-7 7"/></svg>
|
|
</a>
|
|
<a class="btn btn-ghost" href="#pricing" data-scroll>See pricing</a>
|
|
</div>
|
|
</div>
|
|
<div class="hero-asset" data-hero="4">
|
|
<div class="qr-frame">
|
|
<span class="reg reg-tl" aria-hidden="true"></span>
|
|
<span class="reg reg-tr" aria-hidden="true"></span>
|
|
<span class="reg reg-bl" aria-hidden="true"></span>
|
|
<span class="reg reg-br" aria-hidden="true"></span>
|
|
<canvas id="heroQr" width="640" height="640" aria-label="Scannable QR code pointing to qrmaster.net"></canvas>
|
|
<div class="qr-meta">
|
|
<span class="live">Live preview</span>
|
|
</div>
|
|
<div class="qr-echo" id="heroQrEcho">https://www.qrmaster.net</div>
|
|
</div>
|
|
<div class="hero-types">
|
|
<span class="type-chip active" data-type="url">URL</span>
|
|
<span class="type-chip" data-type="vcard">vCard</span>
|
|
<span class="type-chip" data-type="wifi">WiFi</span>
|
|
<span class="type-chip" data-type="event">Event</span>
|
|
<span class="type-chip" data-type="call">Call</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- ==================== STATS STRIP ==================== -->
|
|
<section class="stats" aria-label="Key numbers">
|
|
<div class="container">
|
|
<div class="stats-grid">
|
|
<div class="stat"><div class="stat-num"><em data-count="4.9" data-decimals="1">0</em>/5</div><div class="stat-label">Customer rating</div></div>
|
|
<div class="stat"><div class="stat-num"><span data-count="1412">0</span>+</div><div class="stat-label">Verified reviews</div></div>
|
|
<div class="stat"><div class="stat-num"><span data-count="18">0</span></div><div class="stat-label">Free specialized tools</div></div>
|
|
<div class="stat"><div class="stat-num">€<span data-count="0">0</span></div><div class="stat-label">To start, forever</div></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ==================== GENERATOR STUDIO ==================== -->
|
|
<section class="section" id="generator">
|
|
<div class="container">
|
|
<div class="section-head" data-reveal>
|
|
<h2>Build your first code here.</h2>
|
|
<p>Type a destination, pick colors, check WCAG contrast, and download SVG or PNG. No signup, no watermark.</p>
|
|
</div>
|
|
<div class="gen-wrap">
|
|
<div class="panel gen-panel" data-reveal>
|
|
<div class="field">
|
|
<label for="qrUrl">URL or destination content</label>
|
|
<input class="input" id="qrUrl" type="url" value="https://www.qrmaster.net" placeholder="Enter a URL, text, or destination" spellcheck="false">
|
|
</div>
|
|
<div class="field">
|
|
<label>Color theme</label>
|
|
<div class="swatch-group" role="group" aria-label="Color theme presets">
|
|
<button class="swatch-btn active" style="background:#131310" data-fg="#131310" data-bg="#ffffff" title="Ink Black" aria-label="Ink Black theme" aria-pressed="true"></button>
|
|
<button class="swatch-btn" style="background:#DE3A16" data-fg="#DE3A16" data-bg="#ffffff" title="Signal Red" aria-label="Signal Red theme" aria-pressed="false"></button>
|
|
<button class="swatch-btn" style="background:#1E8A4C" data-fg="#1E8A4C" data-bg="#0F1410" title="Forest Dark" aria-label="Forest Dark theme" aria-pressed="false"></button>
|
|
<button class="swatch-btn" style="background:#274C77" data-fg="#274C77" data-bg="#EAF0F7" title="Slate Blue" aria-label="Slate Blue theme" aria-pressed="false"></button>
|
|
<button class="swatch-btn" style="background:#000000" data-fg="#000000" data-bg="#ffffff" title="Classic Monochrome" aria-label="Classic Monochrome theme" aria-pressed="false"></button>
|
|
</div>
|
|
</div>
|
|
<div class="field-row">
|
|
<div class="field">
|
|
<label for="fgColor">Foreground color</label>
|
|
<div class="color-row">
|
|
<input type="color" id="fgColor" value="#131310" aria-label="Foreground color picker">
|
|
<input class="input" id="fgHex" value="#131310" aria-label="Foreground color hex value" spellcheck="false">
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label for="bgColor">Background color</label>
|
|
<div class="color-row">
|
|
<input type="color" id="bgColor" value="#ffffff" aria-label="Background color picker">
|
|
<input class="input" id="bgHex" value="#ffffff" aria-label="Background color hex value" spellcheck="false">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="field-row">
|
|
<div class="field">
|
|
<label for="cornerStyle">Corner style</label>
|
|
<select class="select" id="cornerStyle">
|
|
<option value="square">Square corners</option>
|
|
<option value="rounded">Rounded corners</option>
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label for="qrSize">Render size</label>
|
|
<input class="range" id="qrSize" type="range" min="120" max="400" value="230" aria-label="QR code size in pixels">
|
|
<div class="range-val"><span id="sizeVal">230</span>px</div>
|
|
</div>
|
|
</div>
|
|
<div class="contrast-row">
|
|
<span class="contrast-badge ok" id="contrastBadge">Good contrast</span>
|
|
<span class="contrast-val">WCAG ratio: <span id="contrastVal">16.2:1</span></span>
|
|
</div>
|
|
<div class="gen-actions">
|
|
<button class="btn btn-ghost" id="dlSvg" type="button">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
|
|
Download SVG
|
|
</button>
|
|
<button class="btn btn-ghost" id="dlPng" type="button">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
|
|
Download PNG
|
|
</button>
|
|
</div>
|
|
<a class="btn btn-primary" href="https://www.qrmaster.net/login">Save and track scans
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14m-7-7 7 7-7 7"/></svg>
|
|
</a>
|
|
</div>
|
|
<div class="gen-preview" data-reveal>
|
|
<div class="qr-stage">
|
|
<span class="corner tl" aria-hidden="true"></span>
|
|
<span class="corner tr" aria-hidden="true"></span>
|
|
<span class="corner bl" aria-hidden="true"></span>
|
|
<span class="corner br" aria-hidden="true"></span>
|
|
<div class="qr-holder" id="qrHolder">
|
|
<canvas id="qrCanvas" width="460" height="460" aria-label="QR code preview canvas"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="qr-url" id="qrUrlEcho">https://www.qrmaster.net</div>
|
|
<div class="qr-note">Scan with any smartphone camera</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ==================== FEATURE BENTO ==================== -->
|
|
<section class="section alt-bg" id="features">
|
|
<div class="container">
|
|
<div class="section-head" data-reveal>
|
|
<h2>What you get for free.</h2>
|
|
</div>
|
|
<div class="bento" data-reveal>
|
|
<div class="bento-cell bento-hero">
|
|
<h3>Edit the destination anytime, without reprinting anything.</h3>
|
|
<p>Dynamic codes route through a short link you control, so the printed code never has to change.</p>
|
|
</div>
|
|
<div class="bento-cell bento-stat">
|
|
<div class="num">3</div>
|
|
<div class="lbl">Dynamic codes on the free plan</div>
|
|
</div>
|
|
<div class="bento-cell bento-stat">
|
|
<div class="num">18</div>
|
|
<div class="lbl">Free specialized generators</div>
|
|
</div>
|
|
<div class="bento-cell bento-cta">
|
|
<h3>See every tool, from WiFi to barcodes.</h3>
|
|
<a class="btn btn-ghost btn-sm" href="#tools" data-scroll>Browse tools
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14m-7-7 7 7-7 7"/></svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ==================== TOOLS INDEX ==================== -->
|
|
<section class="section" id="tools">
|
|
<div class="container">
|
|
<div class="section-head" data-reveal>
|
|
<h2>Every QR type, one generator.</h2>
|
|
<p>18 dedicated tools for every destination. No account required for any of them.</p>
|
|
</div>
|
|
<div class="tools-filter" data-reveal>
|
|
<input class="input" id="toolSearch" type="search" placeholder="Search tools, for example WiFi, vCard, or barcode" aria-label="Search tools">
|
|
</div>
|
|
<div class="tools-index" id="toolsGrid"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ==================== POPULAR WAYS TO USE QR MASTER (merged link-grid) ==================== -->
|
|
<section class="section alt-bg" id="use-cases">
|
|
<div class="container">
|
|
<div class="section-head" data-reveal>
|
|
<h2>Popular ways to use QR Master.</h2>
|
|
<p>Real workflows from restaurants, hotels, agencies, and event teams already running on dynamic codes.</p>
|
|
</div>
|
|
<div class="link-grid" id="useCaseGrid"></div>
|
|
<div class="center-cta" data-reveal>
|
|
<a class="btn btn-ghost" href="https://www.qrmaster.net/use-cases">View all industry guides
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14m-7-7 7 7-7 7"/></svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ==================== INTERACTIVE ROI CALCULATOR ==================== -->
|
|
<section class="section section--tight" id="roi">
|
|
<div class="container">
|
|
<div class="roi-card" data-reveal>
|
|
<div class="roi-left">
|
|
<h2>See what static reprints are <mark>costing you</mark>.</h2>
|
|
<p>Estimate your annual print savings by switching from static printed codes to dynamic ones.</p>
|
|
<div class="field">
|
|
<label for="roiPrints">Total print items, such as menus, flyers, or cards: <strong id="roiPrintsVal">2,500</strong></label>
|
|
<input class="range" id="roiPrints" type="range" min="200" max="25000" step="100" value="2500">
|
|
</div>
|
|
<div class="field">
|
|
<label for="roiUpdates">Annual destination updates: <strong id="roiUpdatesVal">3x per year</strong></label>
|
|
<input class="range" id="roiUpdates" type="range" min="1" max="12" step="1" value="3">
|
|
</div>
|
|
<a class="btn btn-primary" href="https://www.qrmaster.net/reprint-calculator" data-magnetic>Calculate your campaign
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14m-7-7 7 7-7 7"/></svg>
|
|
</a>
|
|
</div>
|
|
<div class="roi-right">
|
|
<span class="lbl">Estimated annual savings</span>
|
|
<span class="big-savings" id="roiSavingsVal">€1,125</span>
|
|
<span class="lbl">Based on an average €0.15 print cost per unit</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ==================== COMPARISON ==================== -->
|
|
<section class="section alt-bg" id="compare">
|
|
<div class="container">
|
|
<div class="section-head" data-reveal>
|
|
<h2>Why switch to dynamic.</h2>
|
|
<p>Stop reprinting physical materials every time a destination changes.</p>
|
|
</div>
|
|
<div class="cmp" data-reveal>
|
|
<div class="cmp-row cmp-head">
|
|
<div></div>
|
|
<div class="ttl-cell"><div class="ttl">Static</div><div class="sub">Always free</div></div>
|
|
<div class="dyn"><div class="ttl">Dynamic</div><div class="sub">Recommended</div></div>
|
|
</div>
|
|
<div class="cmp-row">
|
|
<div class="cmp-cap">Edit the destination anytime<span class="note">No reprint required</span></div>
|
|
<div><span class="cmp-no">Fixed forever</span></div>
|
|
<div><span class="cmp-yes">Instant</span></div>
|
|
</div>
|
|
<div class="cmp-row">
|
|
<div class="cmp-cap">Scan analytics<span class="note">Time, device, location</span></div>
|
|
<div><span class="cmp-no">Not available</span></div>
|
|
<div><span class="cmp-yes">Included</span></div>
|
|
</div>
|
|
<div class="cmp-row">
|
|
<div class="cmp-cap">Custom branding and logo overlay</div>
|
|
<div><span class="cmp-no">Basic only</span></div>
|
|
<div><span class="cmp-yes">Full support</span></div>
|
|
</div>
|
|
<div class="cmp-row">
|
|
<div class="cmp-cap">Bulk creation from a spreadsheet</div>
|
|
<div><span class="cmp-no">Manual only</span></div>
|
|
<div><span class="cmp-yes">Included</span></div>
|
|
</div>
|
|
<div class="cmp-row cmp-foot">
|
|
<div class="cmp-cap">Account required to create<span class="note">Takes under a minute</span></div>
|
|
<div><span class="cmp-yes">No</span></div>
|
|
<div><span class="cmp-no">Free account</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ==================== TESTIMONIALS MARQUEE ==================== -->
|
|
<section class="section testi" id="testimonials">
|
|
<div class="container">
|
|
<div class="section-head" data-reveal>
|
|
<h2>What people are actually saying.</h2>
|
|
</div>
|
|
</div>
|
|
<div class="marquee-mask">
|
|
<div class="marquee-track" id="marqueeTrack"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ==================== PRICING ==================== -->
|
|
<section class="section alt-bg" id="pricing">
|
|
<div class="container">
|
|
<div class="section-head" data-reveal>
|
|
<span class="eyebrow">Simple pricing</span>
|
|
<h2>Choose the plan that fits.</h2>
|
|
<p>No hidden fees. Upgrade or downgrade whenever you need to.</p>
|
|
</div>
|
|
<div style="text-align:center" data-reveal>
|
|
<div class="billing-toggle">
|
|
<span class="opt active" id="optMonthly">Monthly</span>
|
|
<button class="switch" id="billingSwitch" role="switch" aria-checked="false" aria-label="Toggle yearly billing"></button>
|
|
<span class="opt" id="optYearly">Yearly</span>
|
|
<span class="save-tag" id="saveTag">Save 16%</span>
|
|
</div>
|
|
</div>
|
|
<div class="price-grid">
|
|
<div class="panel price-card" data-reveal>
|
|
<div class="price-name">Free</div>
|
|
<div class="price-row"><span class="price-val">€0</span><span class="price-per">Forever</span></div>
|
|
<ul class="price-feats">
|
|
<li>3 active dynamic codes, 8 types</li>
|
|
<li>Unlimited static codes</li>
|
|
<li>Basic scan tracking</li>
|
|
<li>High-res SVG and PNG export</li>
|
|
</ul>
|
|
<a class="btn btn-ghost" href="https://www.qrmaster.net/signup">Get started free</a>
|
|
</div>
|
|
<div class="panel price-card popular" data-reveal>
|
|
<span class="pop-tag">Most popular</span>
|
|
<div class="price-name">Pro</div>
|
|
<div class="price-row"><span class="price-val" data-monthly="€9" data-yearly="€90">€9</span><span class="price-per" data-monthly="per month" data-yearly="per year">per month</span></div>
|
|
<ul class="price-feats">
|
|
<li>50 dynamic codes</li>
|
|
<li>Advanced scan analytics</li>
|
|
<li>Custom branding and logo overlay</li>
|
|
<li>CSV scan report export</li>
|
|
</ul>
|
|
<a class="btn btn-primary" href="https://www.qrmaster.net/signup">Start a 14-day trial</a>
|
|
</div>
|
|
<div class="panel price-card" data-reveal>
|
|
<div class="price-name">Business</div>
|
|
<div class="price-row"><span class="price-val" data-monthly="€29" data-yearly="€290">€29</span><span class="price-per" data-monthly="per month" data-yearly="per year">per month</span></div>
|
|
<ul class="price-feats">
|
|
<li>500 dynamic codes</li>
|
|
<li>Everything in Pro</li>
|
|
<li>Bulk generator, up to 1,000 codes</li>
|
|
<li>Priority support</li>
|
|
</ul>
|
|
<a class="btn btn-ghost" href="https://www.qrmaster.net/signup">Get started</a>
|
|
</div>
|
|
<div class="panel price-card" data-reveal>
|
|
<div class="price-name">Enterprise</div>
|
|
<div class="price-row"><span class="price-val" style="font-size:1.9rem">Custom</span></div>
|
|
<ul class="price-feats">
|
|
<li>Unlimited dynamic codes</li>
|
|
<li>Custom API integration</li>
|
|
<li>Dedicated account manager</li>
|
|
<li>SLA and custom contract</li>
|
|
</ul>
|
|
<a class="btn btn-ghost" href="mailto:timo@qrmaster.net">Contact sales</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ==================== FAQ ==================== -->
|
|
<section class="section" id="faq">
|
|
<div class="container">
|
|
<div class="section-head" data-reveal>
|
|
<h2>Common questions.</h2>
|
|
</div>
|
|
<div class="faq-list">
|
|
<div class="faq-item" data-reveal>
|
|
<button class="faq-q" aria-expanded="false">Do I need an account to create a code?<span class="pm" aria-hidden="true">+</span></button>
|
|
<div class="faq-a"><div><p>No. Static codes can be created without an account. Dynamic codes, which support editing and scan tracking, need a free account.</p></div></div>
|
|
</div>
|
|
<div class="faq-item" data-reveal>
|
|
<button class="faq-q" aria-expanded="false">What is the difference between static and dynamic?<span class="pm" aria-hidden="true">+</span></button>
|
|
<div class="faq-a"><div><p>A static code holds its destination directly in the pattern. A dynamic code routes through a short link, so you can change where it points without changing the printed code.</p></div></div>
|
|
</div>
|
|
<div class="faq-item" data-reveal>
|
|
<button class="faq-q" aria-expanded="false">Do static codes expire?<span class="pm" aria-hidden="true">+</span></button>
|
|
<div class="faq-a"><div><p>No. A static code holds plain data and keeps working for as long as the destination itself stays active.</p></div></div>
|
|
</div>
|
|
<div class="faq-item" data-reveal>
|
|
<button class="faq-q" aria-expanded="false">Which file format should I use for printing?<span class="pm" aria-hidden="true">+</span></button>
|
|
<div class="faq-a"><div><p>SVG, for commercial print. It scales to any size without losing sharpness.</p></div></div>
|
|
</div>
|
|
<div class="faq-item" data-reveal>
|
|
<button class="faq-q" aria-expanded="false">Is QR Master GDPR compliant?<span class="pm" aria-hidden="true">+</span></button>
|
|
<div class="faq-a"><div><p>Yes. Scan analytics use aggregated technical metadata, such as country and device type, and never store personal data.</p></div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ==================== FINAL CTA ==================== -->
|
|
<section class="section final-cta">
|
|
<div class="container">
|
|
<h2 data-reveal>Your first code is thirty seconds away.</h2>
|
|
<p data-reveal>Static codes are free forever. Dynamic codes with analytics start at €0.</p>
|
|
<a class="btn btn-primary" data-reveal data-magnetic href="#generator" data-scroll style="font-size:1.0625rem;padding:16px 34px">Create a QR code
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14m-7-7 7 7-7 7"/></svg>
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ==================== FOOTER ==================== -->
|
|
<footer class="footer">
|
|
<div class="container">
|
|
<div class="footer-grid">
|
|
<div class="footer-brand">
|
|
<a class="logo" href="#top" data-scroll>
|
|
<span class="logo-mark" aria-hidden="true">
|
|
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
|
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
|
|
<rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 14h3v3h-3zM21 14v.01M14 21v.01M21 21v.01M18 18h3"/>
|
|
</svg>
|
|
</span>
|
|
QR Master
|
|
</a>
|
|
<p>Free dynamic QR code generator with tracking, editable destinations, custom branding, and bulk creation.</p>
|
|
</div>
|
|
<div class="footer-col">
|
|
<h4>Product</h4>
|
|
<ul>
|
|
<li><a href="https://www.qrmaster.net/dynamic-qr-code-generator">Dynamic generator</a></li>
|
|
<li><a href="https://www.qrmaster.net/bulk-qr-code-generator">Bulk generator</a></li>
|
|
<li><a href="https://www.qrmaster.net/pricing">Pricing</a></li>
|
|
<li><a href="https://www.qrmaster.net/alternatives">Alternatives</a></li>
|
|
</ul>
|
|
</div>
|
|
<div class="footer-col">
|
|
<h4>Free tools</h4>
|
|
<ul>
|
|
<li><a href="https://www.qrmaster.net/tools/barcode-generator">Barcode generator</a></li>
|
|
<li><a href="https://www.qrmaster.net/tools/wifi-qr-code">WiFi QR code</a></li>
|
|
<li><a href="https://www.qrmaster.net/tools/vcard-qr-code">vCard QR code</a></li>
|
|
<li><a href="https://www.qrmaster.net/tools/google-review-qr-code">Google Review QR</a></li>
|
|
</ul>
|
|
</div>
|
|
<div class="footer-col">
|
|
<h4>Company</h4>
|
|
<ul>
|
|
<li><a href="https://www.qrmaster.net/about">About</a></li>
|
|
<li><a href="https://www.qrmaster.net/blog">Blog</a></li>
|
|
<li><a href="https://www.qrmaster.net/contact">Contact</a></li>
|
|
</ul>
|
|
</div>
|
|
<div class="footer-col">
|
|
<h4>Legal</h4>
|
|
<ul>
|
|
<li><a href="https://www.qrmaster.net/privacy">Privacy policy</a></li>
|
|
<li><a href="https://www.qrmaster.net/terms">Terms of service</a></li>
|
|
<li><a href="https://www.qrmaster.net/cookie-policy">Cookie policy</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="footer-bottom">
|
|
<span>© 2026 QR Master. All rights reserved.</span>
|
|
<span>GDPR compliant · SSL secured</span>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollToPlugin.min.js"></script>
|
|
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
|
|
var REDUCED = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
var FINE_POINTER = window.matchMedia('(pointer: fine)').matches;
|
|
var HAS_GSAP = typeof window.gsap !== 'undefined';
|
|
var HAS_ST = HAS_GSAP && typeof window.ScrollTrigger !== 'undefined';
|
|
if (HAS_ST) gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
|
|
|
|
var root = document.documentElement;
|
|
|
|
/* Mobile Nav Toggle */
|
|
var navBurger = document.getElementById('navBurger');
|
|
var navMobilePanel = document.getElementById('navMobilePanel');
|
|
if (navBurger && navMobilePanel) {
|
|
function closeMobileNav() {
|
|
navBurger.setAttribute('aria-expanded', 'false');
|
|
navMobilePanel.classList.remove('open');
|
|
}
|
|
navBurger.addEventListener('click', function () {
|
|
var open = navBurger.getAttribute('aria-expanded') === 'true';
|
|
navBurger.setAttribute('aria-expanded', String(!open));
|
|
navMobilePanel.classList.toggle('open', !open);
|
|
});
|
|
navMobilePanel.querySelectorAll('[data-mobile-nav]').forEach(function (a) {
|
|
a.addEventListener('click', closeMobileNav);
|
|
});
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape') closeMobileNav();
|
|
});
|
|
window.addEventListener('resize', function () {
|
|
if (window.innerWidth > 900) closeMobileNav();
|
|
}, { passive: true });
|
|
}
|
|
|
|
/* Ambient Halftone Shader Background — ported to vanilla WebGL1 from the
|
|
21st.dev "Halftone Dots" component (paper-design, Apache-2.0), trimmed of
|
|
its cursor-reactivity and driven off this file's --paper/--line/--muted/
|
|
--ink tokens. Motivated: it is a literal halftone dot texture, standing
|
|
in for what a printed QR code physically is, not decoration for its own
|
|
sake. Skipped for reduced motion and narrow viewports. */
|
|
(function halftoneBackground() {
|
|
var canvas = document.getElementById('heroHalftone');
|
|
if (!canvas || REDUCED || window.innerWidth < 700) return;
|
|
var gl = canvas.getContext('webgl', { antialias: false, alpha: true });
|
|
if (!gl) return;
|
|
|
|
var VERT = 'attribute vec2 a_position;\nvoid main(){ gl_Position = vec4(a_position, 0.0, 1.0); }';
|
|
var FRAG = [
|
|
'#ifdef GL_FRAGMENT_PRECISION_HIGH',
|
|
'precision highp float;',
|
|
'#else',
|
|
'precision mediump float;',
|
|
'#endif',
|
|
'uniform vec3 u_colors[8];',
|
|
'uniform vec4 u_scene;',
|
|
'uniform vec4 u_shape;',
|
|
'uniform vec4 u_surface;',
|
|
'uniform vec4 u_finish;',
|
|
'uniform vec4 u_transform;',
|
|
'#define u_resolution u_scene.xy',
|
|
'#define u_time u_scene.z',
|
|
'#define u_colorCount u_scene.w',
|
|
'#define u_scale u_shape.x',
|
|
'#define u_intensity u_shape.y',
|
|
'#define u_paramA u_shape.z',
|
|
'#define u_warp u_shape.w',
|
|
'#define u_detail u_surface.x',
|
|
'#define u_contrast u_surface.y',
|
|
'#define u_brightness u_surface.z',
|
|
'#define u_saturation u_surface.w',
|
|
'#define u_hue u_finish.x',
|
|
'#define u_vignette u_finish.y',
|
|
'#define u_blur u_finish.z',
|
|
'#define u_grain u_finish.w',
|
|
'#ifdef GL_FRAGMENT_PRECISION_HIGH',
|
|
'#define u_seed u_transform.x',
|
|
'#else',
|
|
'#define u_seed mod(u_transform.x, 31.0)',
|
|
'#endif',
|
|
'#define u_rotate u_transform.y',
|
|
'#define u_drift u_transform.z',
|
|
'#define u_oklab u_transform.w',
|
|
'float hash21(vec2 p) {',
|
|
' #ifndef GL_FRAGMENT_PRECISION_HIGH',
|
|
' p = mod(p, 31.0);',
|
|
' #endif',
|
|
' p = fract(p * vec2(234.34, 435.345));',
|
|
' p += dot(p, p + 34.23);',
|
|
' return fract(p.x * p.y);',
|
|
'}',
|
|
'float grainHash(vec2 p) {',
|
|
' vec3 p3 = fract(vec3(p.xyx) * 0.1031);',
|
|
' p3 += dot(p3, p3.yzx + 33.33);',
|
|
' return fract((p3.x + p3.y) * p3.z);',
|
|
'}',
|
|
'float noise(vec2 p) {',
|
|
' vec2 i = floor(p); vec2 f = fract(p);',
|
|
' vec2 u = f * f * (3.0 - 2.0 * f);',
|
|
' return mix(mix(hash21(i), hash21(i + vec2(1.0,0.0)), u.x),',
|
|
' mix(hash21(i + vec2(0.0,1.0)), hash21(i + vec2(1.0,1.0)), u.x), u.y);',
|
|
'}',
|
|
'float fbm(vec2 p) {',
|
|
' float v = 0.0; float a = 0.5;',
|
|
' for (int i = 0; i < 5; i++) { v += a * noise(p); p = p * 2.03 + vec2(17.0, 9.2); a *= 0.5; }',
|
|
' return v;',
|
|
'}',
|
|
'vec3 srgbToLinear(vec3 c) { return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c)); }',
|
|
'vec3 linearToSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(max(c, vec3(0.0)), vec3(1.0/2.4)) - 0.055, step(0.0031308, c)); }',
|
|
'vec3 linToOklab(vec3 c) {',
|
|
' float l = 0.4122214708*c.r + 0.5363325363*c.g + 0.0514459929*c.b;',
|
|
' float m = 0.2119034982*c.r + 0.6806995451*c.g + 0.1073969566*c.b;',
|
|
' float s = 0.0883024619*c.r + 0.2817188376*c.g + 0.6299787005*c.b;',
|
|
' l = pow(max(l,0.0),1.0/3.0); m = pow(max(m,0.0),1.0/3.0); s = pow(max(s,0.0),1.0/3.0);',
|
|
' return vec3(0.2104542553*l + 0.7936177850*m - 0.0040720468*s,',
|
|
' 1.9779984951*l - 2.4285922050*m + 0.4505937099*s,',
|
|
' 0.0259040371*l + 0.7827717662*m - 0.8086757660*s);',
|
|
'}',
|
|
'vec3 oklabToLin(vec3 c) {',
|
|
' float l = c.x + 0.3963377774*c.y + 0.2158037573*c.z;',
|
|
' float m = c.x - 0.1055613458*c.y - 0.0638541728*c.z;',
|
|
' float s = c.x - 0.0894841775*c.y - 1.2914855480*c.z;',
|
|
' l=l*l*l; m=m*m*m; s=s*s*s;',
|
|
' return vec3(4.0767416621*l - 3.3077115913*m + 0.2309699292*s,',
|
|
' -1.2684380046*l + 2.6097574011*m - 0.3413193965*s,',
|
|
' -0.0041960863*l - 0.7034186147*m + 1.7076147010*s);',
|
|
'}',
|
|
'vec3 mixColour(vec3 a, vec3 b, float t) {',
|
|
' if (u_oklab > 0.5) {',
|
|
' vec3 la = linToOklab(srgbToLinear(a)); vec3 lb = linToOklab(srgbToLinear(b));',
|
|
' return clamp(linearToSrgb(oklabToLin(mix(la, lb, t))), 0.0, 1.0);',
|
|
' }',
|
|
' return mix(a, b, t);',
|
|
'}',
|
|
'vec3 palette(float x) {',
|
|
' float n = max(u_colorCount - 1.0, 1.0);',
|
|
' float f = clamp(x, 0.0, 1.0) * n;',
|
|
' vec3 col = u_colors[0];',
|
|
' for (int i = 0; i < 7; i++) {',
|
|
' if (float(i) < n) col = mixColour(col, u_colors[i+1], smoothstep(0.0, 1.0, clamp(f - float(i), 0.0, 1.0)));',
|
|
' }',
|
|
' return col;',
|
|
'}',
|
|
'vec3 hueRotate(vec3 col, float a) {',
|
|
' const mat3 toYIQ = mat3(0.299,0.596,0.211, 0.587,-0.274,-0.523, 0.114,-0.322,0.312);',
|
|
' const mat3 toRGB = mat3(1.0,1.0,1.0, 0.956,-0.272,-1.106, 0.621,-0.647,1.703);',
|
|
' vec3 yiq = toYIQ * col; float ca = cos(a), sa = sin(a);',
|
|
' yiq = vec3(yiq.x, yiq.y*ca - yiq.z*sa, yiq.y*sa + yiq.z*ca);',
|
|
' return toRGB * yiq;',
|
|
'}',
|
|
'vec3 shade(vec2 uv, vec2 p, float t) {',
|
|
' float cells = 13.0 + u_intensity * 42.0;',
|
|
' vec2 grid = p * cells; vec2 id = floor(grid); vec2 local = fract(grid) - 0.5;',
|
|
' if (mod(id.y, 2.0) > 0.5) local.x += 0.5;',
|
|
' local.x = fract(local.x + 0.5) - 0.5;',
|
|
' float source = fbm(p * 2.1 + vec2(t * 0.025, -t * 0.018) + u_seed);',
|
|
' source = mix(source, 0.5 + 0.5 * sin(p.x * 2.7 + p.y * 1.9), 0.35);',
|
|
' float radius = (0.08 + source * 0.36) * mix(0.55, 1.45, u_paramA);',
|
|
' float grain = (hash21(id + u_seed) - 0.5) * u_intensity * 0.08;',
|
|
' float dotMask = 1.0 - smoothstep(radius - 0.055, radius + 0.025, length(local) + grain);',
|
|
' return mix(u_colors[0], palette(source), dotMask);',
|
|
'}',
|
|
'void main() {',
|
|
' vec2 uv = gl_FragCoord.xy / u_resolution.xy;',
|
|
' vec2 screenUv = uv;',
|
|
' vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.x, u_resolution.y);',
|
|
' uv = p * min(u_resolution.x, u_resolution.y) / u_resolution.xy + 0.5;',
|
|
' p *= u_scale;',
|
|
' if (abs(u_rotate) > 0.0001) { float cr = cos(u_rotate), sr = sin(u_rotate); p = mat2(cr,-sr,sr,cr) * p; }',
|
|
' if (u_drift > 0.0001) p += u_drift * vec2(sin(u_time * 0.31), cos(u_time * 0.23));',
|
|
' if (u_warp > 0.0) p += u_warp * (vec2(fbm(p * u_detail + u_seed), fbm(p * u_detail + vec2(5.2,1.3))) - 0.5);',
|
|
' vec3 col;',
|
|
' if (u_blur > 0.0) {',
|
|
' float e = u_blur; float pe = e * u_scale;',
|
|
' vec2 uvE = vec2(e) * min(u_resolution.x, u_resolution.y) / u_resolution.xy;',
|
|
' col = shade(uv, p, u_time) * 0.36;',
|
|
' col += shade(uv + vec2(uvE.x,0.0), p + vec2(pe,0.0), u_time) * 0.16;',
|
|
' col += shade(uv - vec2(uvE.x,0.0), p - vec2(pe,0.0), u_time) * 0.16;',
|
|
' col += shade(uv + vec2(0.0,uvE.y), p + vec2(0.0,pe), u_time) * 0.16;',
|
|
' col += shade(uv - vec2(0.0,uvE.y), p - vec2(0.0,pe), u_time) * 0.16;',
|
|
' } else { col = shade(uv, p, u_time); }',
|
|
' if (abs(u_contrast - 1.0) > 0.0001) col = (col - 0.5) * u_contrast + 0.5;',
|
|
' if (abs(u_saturation - 1.0) > 0.0001) { float luma = dot(col, vec3(0.299,0.587,0.114)); col = mix(vec3(luma), col, u_saturation); }',
|
|
' if (abs(u_hue) > 0.0001) col = hueRotate(col, u_hue);',
|
|
' if (abs(u_brightness) > 0.0001) col += u_brightness;',
|
|
' if (u_vignette > 0.0001) { float vd = length(screenUv - 0.5) * 1.41421356; col *= 1.0 - u_vignette * smoothstep(0.35, 1.0, vd); }',
|
|
' if (u_grain > 0.0001) col += (grainHash(gl_FragCoord.xy + vec2(u_seed * 17.0, u_seed * 31.0)) - 0.5) * u_grain;',
|
|
' gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);',
|
|
'}'
|
|
].join('\n');
|
|
|
|
function compile(type, src) {
|
|
var s = gl.createShader(type);
|
|
gl.shaderSource(s, src);
|
|
gl.compileShader(s);
|
|
return s;
|
|
}
|
|
var program = gl.createProgram();
|
|
gl.attachShader(program, compile(gl.VERTEX_SHADER, VERT));
|
|
gl.attachShader(program, compile(gl.FRAGMENT_SHADER, FRAG));
|
|
gl.linkProgram(program);
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return;
|
|
gl.useProgram(program);
|
|
|
|
var buf = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
var posLoc = gl.getAttribLocation(program, 'a_position');
|
|
gl.enableVertexAttribArray(posLoc);
|
|
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
|
|
|
|
var uni = {
|
|
colors: gl.getUniformLocation(program, 'u_colors'),
|
|
scene: gl.getUniformLocation(program, 'u_scene'),
|
|
shape: gl.getUniformLocation(program, 'u_shape'),
|
|
surface: gl.getUniformLocation(program, 'u_surface'),
|
|
finish: gl.getUniformLocation(program, 'u_finish'),
|
|
transform: gl.getUniformLocation(program, 'u_transform')
|
|
};
|
|
|
|
var S = {
|
|
scale: 1.05, intensity: 0.28, paramA: 0.30, warp: 0.14,
|
|
detail: 1.6, contrast: 1.0, brightness: 0.0, saturation: 1.0,
|
|
hue: 0.0, vignette: 0.3, blur: 0.0, grain: 0.02,
|
|
seed: 3.0, rotate: 0.0, drift: 0.05, oklab: 0.0,
|
|
timeScale: 0.1, colorCount: 4
|
|
};
|
|
|
|
function hexToRgb(hex) {
|
|
var v = parseInt((hex || '').trim().replace('#', ''), 16) || 0;
|
|
return [((v >> 16) & 255) / 255, ((v >> 8) & 255) / 255, (v & 255) / 255];
|
|
}
|
|
function uploadColors() {
|
|
var cs = getComputedStyle(document.documentElement);
|
|
var stops = [
|
|
cs.getPropertyValue('--paper') || '#F4F5F1',
|
|
cs.getPropertyValue('--line') || '#DEDCD0',
|
|
cs.getPropertyValue('--muted') || '#82806F',
|
|
cs.getPropertyValue('--ink') || '#131310'
|
|
].map(hexToRgb);
|
|
while (stops.length < 8) stops.push(stops[stops.length - 1]);
|
|
var flat = [];
|
|
stops.forEach(function (c) { flat.push(c[0], c[1], c[2]); });
|
|
gl.uniform3fv(uni.colors, new Float32Array(flat));
|
|
}
|
|
uploadColors();
|
|
gl.uniform4f(uni.shape, S.scale, S.intensity, S.paramA, S.warp);
|
|
gl.uniform4f(uni.surface, S.detail, S.contrast, S.brightness, S.saturation);
|
|
gl.uniform4f(uni.finish, S.hue, S.vignette, S.blur, S.grain);
|
|
gl.uniform4f(uni.transform, S.seed, S.rotate, S.drift, S.oklab);
|
|
|
|
var start = performance.now();
|
|
var raf = 0;
|
|
var visible = true;
|
|
|
|
function resize() {
|
|
var dpr = Math.min(window.devicePixelRatio || 1, 1.5);
|
|
var w = Math.max(1, Math.round(canvas.clientWidth * dpr));
|
|
var h = Math.max(1, Math.round(canvas.clientHeight * dpr));
|
|
if (canvas.width !== w || canvas.height !== h) {
|
|
canvas.width = w; canvas.height = h;
|
|
gl.viewport(0, 0, w, h);
|
|
}
|
|
}
|
|
function requestFrame() {
|
|
if (!raf && visible) raf = requestAnimationFrame(render);
|
|
}
|
|
function render(now) {
|
|
raf = 0;
|
|
if (!visible) return;
|
|
resize();
|
|
gl.uniform4f(uni.scene, canvas.width, canvas.height, ((now - start) / 1000) * S.timeScale, S.colorCount);
|
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
requestFrame();
|
|
}
|
|
|
|
var io = new IntersectionObserver(function (entries) {
|
|
visible = entries[0] ? entries[0].isIntersecting : true;
|
|
if (visible) requestFrame();
|
|
});
|
|
io.observe(canvas);
|
|
document.addEventListener('visibilitychange', function () {
|
|
visible = document.visibilityState === 'visible' && visible;
|
|
if (visible) requestFrame();
|
|
});
|
|
window.addEventListener('resize', function () { resize(); requestFrame(); }, { passive: true });
|
|
|
|
var themeObserver = new MutationObserver(function () { uploadColors(); requestFrame(); });
|
|
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
|
|
requestFrame();
|
|
})();
|
|
|
|
/* Theme Switcher */
|
|
var storedTheme = null;
|
|
try { storedTheme = localStorage.getItem('qm-theme'); } catch (e) {}
|
|
if (storedTheme === 'light' || storedTheme === 'dark') root.setAttribute('data-theme', storedTheme);
|
|
|
|
document.getElementById('themeToggle').addEventListener('click', function () {
|
|
var next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
|
root.setAttribute('data-theme', next);
|
|
try { localStorage.setItem('qm-theme', next); } catch (e) {}
|
|
});
|
|
|
|
/* Nav & Scroll Progress — GSAP ScrollTrigger, no manual scroll listeners */
|
|
var nav = document.getElementById('nav');
|
|
var progressBar = document.getElementById('progressBar');
|
|
if (HAS_ST) {
|
|
ScrollTrigger.create({
|
|
start: 0, end: 'max',
|
|
onUpdate: function (self) {
|
|
nav.classList.toggle('scrolled', self.scroll() > 20);
|
|
gsap.set(progressBar, { scaleX: self.progress });
|
|
}
|
|
});
|
|
} else if (progressBar) {
|
|
progressBar.style.transform = 'scaleX(0)';
|
|
}
|
|
|
|
/* Smooth Scrolling */
|
|
document.querySelectorAll('[data-scroll]').forEach(function (a) {
|
|
a.addEventListener('click', function (e) {
|
|
var id = a.getAttribute('href');
|
|
if (!id || id.charAt(0) !== '#') return;
|
|
var target = document.querySelector(id);
|
|
if (!target) return;
|
|
e.preventDefault();
|
|
if (HAS_ST && !REDUCED) {
|
|
gsap.to(window, { duration: 0.8, ease: 'power3.inOut', scrollTo: { y: target, offsetY: 72 } });
|
|
} else {
|
|
target.scrollIntoView({ behavior: REDUCED ? 'auto' : 'smooth', block: 'start' });
|
|
}
|
|
});
|
|
});
|
|
|
|
/* QR Core Rendering helper */
|
|
var HAS_QR = typeof window.qrcode !== 'undefined';
|
|
function makeQr(text) {
|
|
if (!HAS_QR) return null;
|
|
try {
|
|
var qr = window.qrcode(0, 'M');
|
|
qr.addData(text);
|
|
qr.make();
|
|
return qr;
|
|
} catch (e) { return null; }
|
|
}
|
|
function paintQr(canvas, qr, fg, bg, marginModules) {
|
|
var ctx = canvas.getContext('2d');
|
|
var n = qr.getModuleCount(), m = marginModules || 4;
|
|
var cell = canvas.width / (n + m * 2);
|
|
ctx.fillStyle = bg;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
ctx.fillStyle = fg;
|
|
for (var row = 0; row < n; row++) {
|
|
for (var col = 0; col < n; col++) {
|
|
if (qr.isDark(row, col)) {
|
|
ctx.fillRect(Math.floor((col + m) * cell), Math.floor((row + m) * cell), Math.ceil(cell), Math.ceil(cell));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Render Hero QR */
|
|
var heroCanvas = document.getElementById('heroQr');
|
|
var heroQrEcho = document.getElementById('heroQrEcho');
|
|
function updateHeroQr(text) {
|
|
if (!heroCanvas) return;
|
|
var qr = makeQr(text);
|
|
if (qr) paintQr(heroCanvas, qr, '#131310', '#ffffff', 4);
|
|
if (heroQrEcho) heroQrEcho.textContent = text;
|
|
}
|
|
updateHeroQr('https://www.qrmaster.net');
|
|
|
|
/* Hero Type Selector Chips */
|
|
document.querySelectorAll('.type-chip').forEach(function (chip) {
|
|
chip.addEventListener('click', function () {
|
|
document.querySelectorAll('.type-chip').forEach(function (c) { c.classList.remove('active'); });
|
|
chip.classList.add('active');
|
|
var type = chip.getAttribute('data-type');
|
|
var sampleUrls = {
|
|
url: 'https://www.qrmaster.net',
|
|
vcard: 'BEGIN:VCARD\nFN:Timo Master\nTEL:+49123456789\nEND:VCARD',
|
|
wifi: 'WIFI:S:MyNetwork;T:WPA;P:SecretPassword;;',
|
|
event: 'https://www.qrmaster.net/event-registration',
|
|
call: 'tel:+49123456789'
|
|
};
|
|
var target = sampleUrls[type] || 'https://www.qrmaster.net';
|
|
updateHeroQr(target);
|
|
});
|
|
});
|
|
|
|
var ARROW = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17 17 7M8 7h9v9"/></svg>';
|
|
|
|
var TOOLS = [
|
|
{ name: 'URL generator', desc: 'Open any web destination', href: 'https://www.qrmaster.net/tools/url-qr-code' },
|
|
{ name: 'vCard contact', desc: 'Share contact details instantly', href: 'https://www.qrmaster.net/tools/vcard-qr-code' },
|
|
{ name: 'WiFi connect', desc: 'Join a network without typing a password', href: 'https://www.qrmaster.net/tools/wifi-qr-code' },
|
|
{ name: 'Plain text', desc: 'Display unformatted text', href: 'https://www.qrmaster.net/tools/text-qr-code' },
|
|
{ name: 'Email dispatch', desc: 'Pre-fill a recipient and subject', href: 'https://www.qrmaster.net/tools/email-qr-code' },
|
|
{ name: 'SMS text', desc: 'Send a pre-written text message', href: 'https://www.qrmaster.net/tools/sms-qr-code' },
|
|
{ name: 'Crypto wallet', desc: 'Receive crypto payments', href: 'https://www.qrmaster.net/tools/crypto-qr-code' },
|
|
{ name: 'Calendar event', desc: 'Save an event to a calendar', href: 'https://www.qrmaster.net/tools/event-qr-code' },
|
|
{ name: 'Facebook page', desc: 'Direct scans to a Facebook page', href: 'https://www.qrmaster.net/tools/facebook-qr-code' },
|
|
{ name: 'Instagram', desc: 'Open an Instagram profile', href: 'https://www.qrmaster.net/tools/instagram-qr-code' },
|
|
{ name: 'YouTube channel', desc: 'Open a video or a channel', href: 'https://www.qrmaster.net/tools/youtube-qr-code' },
|
|
{ name: 'WhatsApp message', desc: 'Start a WhatsApp chat', href: 'https://www.qrmaster.net/tools/whatsapp-qr-code' },
|
|
{ name: 'TikTok link', desc: 'Open a TikTok profile', href: 'https://www.qrmaster.net/tools/tiktok-qr-code' },
|
|
{ name: 'Geolocation', desc: 'Share map coordinates', href: 'https://www.qrmaster.net/tools/geolocation-qr-code' },
|
|
{ name: 'Phone call', desc: 'Start a direct phone call', href: 'https://www.qrmaster.net/tools/call-qr-code-generator' },
|
|
{ name: 'Google Review', desc: 'Collect customer reviews', href: 'https://www.qrmaster.net/tools/google-review-qr-code' },
|
|
{ name: 'Barcode generator', desc: 'Create EAN and UPC barcodes', href: 'https://www.qrmaster.net/tools/barcode-generator' },
|
|
{ name: 'Teams meeting', desc: 'Join a Microsoft Teams call', href: 'https://www.qrmaster.net/tools/teams-qr-code' }
|
|
];
|
|
|
|
var USE_CASES = [
|
|
{ title: 'Bulk generator', desc: 'Generate thousands of unique codes from a spreadsheet.', href: 'https://www.qrmaster.net/bulk-qr-code-generator' },
|
|
{ title: 'Hotels and hospitality', desc: 'Route guests to a current room-service menu or check-in flow.', href: 'https://www.qrmaster.net/use-cases/qr-codes-for-hotel' },
|
|
{ title: 'Barbershops and salons', desc: 'Keep a booking link current without reprinting cards.', href: 'https://www.qrmaster.net/use-cases/qr-codes-for-barbershops' },
|
|
{ title: 'Business cards', desc: 'Direct contacts to an up-to-date vCard or booking page.', href: 'https://www.qrmaster.net/use-cases/business-card-qr-codes' },
|
|
{ title: 'Events and conferences', desc: 'Manage schedules and venue maps live, on event day.', href: 'https://www.qrmaster.net/use-cases/event-qr-codes' },
|
|
{ title: 'Print flyers and posters', desc: 'Compare flyer placements and rotate an offer without reprinting.', href: 'https://www.qrmaster.net/use-cases/flyer-qr-codes' },
|
|
{ title: 'Product packaging', desc: 'Connect a package insert to a manual or a support chat.', href: 'https://www.qrmaster.net/use-cases/packaging-qr-codes' },
|
|
{ title: 'Real estate signs', desc: 'Update a listing instantly on an outdoor yard sign.', href: 'https://www.qrmaster.net/use-cases/real-estate-sign-qr-codes' }
|
|
];
|
|
|
|
var TESTIMONIALS = [
|
|
{ rating: 5, title: 'Perfect for my pottery shop', content: 'I put a code directly on each piece so buyers can reach my site and contact details instantly.', name: 'Claudia', role: 'Studio owner, Texas' },
|
|
{ rating: 5, title: 'No more reprinting menus', content: 'Prices change, I update the link, and every table code updates with it. That alone saved us hundreds.', name: 'Thomas B.', role: 'Restaurant owner, Hamburg' },
|
|
{ rating: 5, title: 'Measurable ROI on print', content: 'Scan analytics finally show us which flyer and location actually drove traffic.', name: 'Sarah M.', role: 'Head of Marketing, Berlin' },
|
|
{ rating: 5, title: '800 unique codes in minutes', content: 'I uploaded one spreadsheet and generated all 800 packaging codes at once.', name: 'Jennifer K.', role: 'Operations Manager, London' },
|
|
{ rating: 5, title: 'Essential for our events', content: 'When a venue schedule changes last minute, we update the destination instead of the posters.', name: 'Marco F.', role: 'Event Director, Munich' },
|
|
{ rating: 5, title: 'Passed our compliance review', content: 'Our legal team approved it because IP addresses are hashed and no personal data is stored.', name: 'Stefan W.', role: 'Data Protection Officer, Frankfurt' }
|
|
];
|
|
|
|
function el(htmlStr) {
|
|
var t = document.createElement('template');
|
|
t.innerHTML = htmlStr.trim();
|
|
return t.content.firstChild;
|
|
}
|
|
|
|
var toolsGrid = document.getElementById('toolsGrid');
|
|
function renderTools(filterText) {
|
|
if (!toolsGrid) return;
|
|
toolsGrid.innerHTML = '';
|
|
var q = (filterText || '').toLowerCase().trim();
|
|
var count = 0;
|
|
TOOLS.forEach(function (t) {
|
|
if (q && t.name.toLowerCase().indexOf(q) === -1 && t.desc.toLowerCase().indexOf(q) === -1) return;
|
|
count++;
|
|
toolsGrid.appendChild(el(
|
|
'<a class="tool-row" href="' + t.href + '" data-reveal>' +
|
|
'<span class="idx">' + ('0' + count).slice(-2) + '</span>' +
|
|
'<h3>' + t.name + '</h3><p>' + t.desc + '</p>' +
|
|
'<span class="arr">' + ARROW + '</span>' +
|
|
'</a>'
|
|
));
|
|
});
|
|
if (count === 0) {
|
|
toolsGrid.innerHTML = '<div style="padding:32px;text-align:center;color:var(--muted);font-family:var(--font-mono);font-size:12px;">No matching tools. Try searching "WiFi", "vCard", or "URL".</div>';
|
|
}
|
|
}
|
|
renderTools('');
|
|
var toolSearch = document.getElementById('toolSearch');
|
|
if (toolSearch) toolSearch.addEventListener('input', function () { renderTools(toolSearch.value); });
|
|
|
|
var useCaseGrid = document.getElementById('useCaseGrid');
|
|
if (useCaseGrid) {
|
|
USE_CASES.forEach(function (item) {
|
|
useCaseGrid.appendChild(el(
|
|
'<a class="panel panel-hover link-card" href="' + item.href + '" data-reveal>' +
|
|
'<span class="go">' + ARROW + '</span>' +
|
|
'<h3>' + item.title + '</h3><p>' + item.desc + '</p>' +
|
|
'</a>'
|
|
));
|
|
});
|
|
}
|
|
|
|
function stars(rating) {
|
|
var s = '<div class="stars" role="img" aria-label="' + rating + ' out of 5 stars">';
|
|
for (var i = 0; i < 5; i++) s += i < rating ? '★' : '<span class="off">★</span>';
|
|
return s + '</div>';
|
|
}
|
|
var track = document.getElementById('marqueeTrack');
|
|
if (track) {
|
|
TESTIMONIALS.concat(TESTIMONIALS).forEach(function (t) {
|
|
track.appendChild(el(
|
|
'<div class="panel testi-card">' +
|
|
'<div class="testi-top">' + stars(t.rating) + '</div>' +
|
|
'<h3>' + t.title + '</h3>' +
|
|
'<p>' + t.content + '</p>' +
|
|
'<div class="testi-author"><b>' + t.name + '</b><span>' + t.role + '</span></div>' +
|
|
'</div>'
|
|
));
|
|
});
|
|
}
|
|
|
|
/* Interactive Generator Studio */
|
|
(function generator() {
|
|
var urlInput = document.getElementById('qrUrl');
|
|
var fgColor = document.getElementById('fgColor'), fgHex = document.getElementById('fgHex');
|
|
var bgColor = document.getElementById('bgColor'), bgHex = document.getElementById('bgHex');
|
|
var corners = document.getElementById('cornerStyle');
|
|
var sizeInput = document.getElementById('qrSize'), sizeVal = document.getElementById('sizeVal');
|
|
var holder = document.getElementById('qrHolder');
|
|
var canvas = document.getElementById('qrCanvas');
|
|
var echo = document.getElementById('qrUrlEcho');
|
|
var badge = document.getElementById('contrastBadge');
|
|
var contrastVal = document.getElementById('contrastVal');
|
|
var debounce = null;
|
|
|
|
function normHex(v) {
|
|
v = (v || '').trim();
|
|
if (v.charAt(0) !== '#') v = '#' + v;
|
|
return /^#[0-9a-fA-F]{6}$/.test(v) ? v.toLowerCase() : null;
|
|
}
|
|
function luminance(hex) {
|
|
var r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
var g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
var b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
function ch(v) { return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }
|
|
return 0.2126 * ch(r) + 0.7152 * ch(g) + 0.0722 * ch(b);
|
|
}
|
|
function contrast(a, b) {
|
|
var l1 = luminance(a), l2 = luminance(b);
|
|
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
}
|
|
function state() {
|
|
return {
|
|
url: urlInput ? urlInput.value.trim() : 'https://www.qrmaster.net',
|
|
fg: normHex(fgHex.value) || '#131310',
|
|
bg: normHex(bgHex.value) || '#ffffff',
|
|
size: parseInt(sizeInput.value, 10) || 230
|
|
};
|
|
}
|
|
function render() {
|
|
if (!canvas) return;
|
|
var s = state();
|
|
if (sizeVal) sizeVal.textContent = s.size;
|
|
if (echo) echo.textContent = s.url || 'https://www.qrmaster.net';
|
|
if (holder) holder.style.borderRadius = corners.value === 'rounded' ? '12px' : '0px';
|
|
|
|
var c = contrast(s.fg, s.bg);
|
|
var ok = c >= 4.5;
|
|
if (badge) {
|
|
badge.textContent = ok ? 'Good contrast' : 'Low contrast';
|
|
badge.className = 'contrast-badge ' + (ok ? 'ok' : 'bad');
|
|
}
|
|
if (contrastVal) contrastVal.textContent = c.toFixed(1) + ':1';
|
|
|
|
var scale = 2;
|
|
canvas.style.width = s.size + 'px';
|
|
canvas.style.height = s.size + 'px';
|
|
canvas.width = s.size * scale;
|
|
canvas.height = s.size * scale;
|
|
|
|
var qr = makeQr(s.url || 'https://www.qrmaster.net');
|
|
if (!qr) return;
|
|
paintQr(canvas, qr, s.fg, s.bg, 4);
|
|
}
|
|
function queue() {
|
|
clearTimeout(debounce);
|
|
debounce = setTimeout(render, 120);
|
|
}
|
|
function bindColor(picker, hex) {
|
|
if (!picker || !hex) return;
|
|
picker.addEventListener('input', function () { hex.value = picker.value; queue(); });
|
|
hex.addEventListener('input', function () {
|
|
var v = normHex(hex.value);
|
|
if (v) picker.value = v;
|
|
queue();
|
|
});
|
|
}
|
|
bindColor(fgColor, fgHex);
|
|
bindColor(bgColor, bgHex);
|
|
|
|
var swatchBtns = document.querySelectorAll('.swatch-btn');
|
|
swatchBtns.forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var fg = btn.getAttribute('data-fg');
|
|
var bg = btn.getAttribute('data-bg');
|
|
if (fg && fgColor && fgHex) { fgColor.value = fg; fgHex.value = fg; }
|
|
if (bg && bgColor && bgHex) { bgColor.value = bg; bgHex.value = bg; }
|
|
swatchBtns.forEach(function (b) { b.classList.remove('active'); b.setAttribute('aria-pressed', 'false'); });
|
|
btn.classList.add('active');
|
|
btn.setAttribute('aria-pressed', 'true');
|
|
render();
|
|
});
|
|
});
|
|
if (fgHex) {
|
|
fgHex.addEventListener('input', function () {
|
|
swatchBtns.forEach(function (b) { b.classList.remove('active'); b.setAttribute('aria-pressed', 'false'); });
|
|
});
|
|
}
|
|
if (urlInput) urlInput.addEventListener('input', queue);
|
|
if (corners) corners.addEventListener('change', queue);
|
|
if (sizeInput) sizeInput.addEventListener('input', queue);
|
|
|
|
function triggerDownload(href, name) {
|
|
var a = document.createElement('a');
|
|
a.href = href; a.download = name;
|
|
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
|
setTimeout(function () { URL.revokeObjectURL(href); }, 1000);
|
|
}
|
|
var dlSvg = document.getElementById('dlSvg');
|
|
if (dlSvg) {
|
|
dlSvg.addEventListener('click', function () {
|
|
var s = state();
|
|
var qr = makeQr(s.url || 'https://www.qrmaster.net');
|
|
if (!qr) return;
|
|
var n = qr.getModuleCount(), margin = 4, cell = 10;
|
|
var dim = (n + margin * 2) * cell;
|
|
var rects = '';
|
|
for (var row = 0; row < n; row++) {
|
|
for (var col = 0; col < n; col++) {
|
|
if (qr.isDark(row, col)) {
|
|
rects += '<rect x="' + ((col + margin) * cell) + '" y="' + ((row + margin) * cell) + '" width="' + cell + '" height="' + cell + '"/>';
|
|
}
|
|
}
|
|
}
|
|
var svg = '<?xml version="1.0" encoding="UTF-8"?>\n' +
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="' + dim + '" height="' + dim + '" viewBox="0 0 ' + dim + ' ' + dim + '">' +
|
|
'<rect width="' + dim + '" height="' + dim + '" fill="' + s.bg + '"/>' +
|
|
'<g fill="' + s.fg + '">' + rects + '</g></svg>';
|
|
triggerDownload(URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' })), 'qrcode.svg');
|
|
});
|
|
}
|
|
var dlPng = document.getElementById('dlPng');
|
|
if (dlPng) {
|
|
dlPng.addEventListener('click', function () {
|
|
var s = state();
|
|
var qr = makeQr(s.url || 'https://www.qrmaster.net');
|
|
if (!qr) return;
|
|
var out = document.createElement('canvas');
|
|
var px = Math.max(s.size, 512);
|
|
out.width = px; out.height = px;
|
|
paintQr(out, qr, s.fg, s.bg, 4);
|
|
out.toBlob(function (blob) {
|
|
if (blob) triggerDownload(URL.createObjectURL(blob), 'qrcode.png');
|
|
});
|
|
});
|
|
}
|
|
render();
|
|
})();
|
|
|
|
/* Interactive ROI Calculator */
|
|
(function roiCalculator() {
|
|
var printsInput = document.getElementById('roiPrints');
|
|
var updatesInput = document.getElementById('roiUpdates');
|
|
var printsVal = document.getElementById('roiPrintsVal');
|
|
var updatesVal = document.getElementById('roiUpdatesVal');
|
|
var savingsVal = document.getElementById('roiSavingsVal');
|
|
function updateRoi() {
|
|
if (!printsInput || !updatesInput) return;
|
|
var prints = parseInt(printsInput.value, 10);
|
|
var updates = parseInt(updatesInput.value, 10);
|
|
if (printsVal) printsVal.textContent = prints.toLocaleString();
|
|
if (updatesVal) updatesVal.textContent = updates + 'x per year';
|
|
var annualSavings = Math.round(prints * updates * 0.15);
|
|
if (savingsVal) savingsVal.textContent = '€' + annualSavings.toLocaleString();
|
|
}
|
|
if (printsInput) printsInput.addEventListener('input', updateRoi);
|
|
if (updatesInput) updatesInput.addEventListener('input', updateRoi);
|
|
updateRoi();
|
|
})();
|
|
|
|
/* Widgets: Pricing Toggle, FAQ Accordion */
|
|
(function widgets() {
|
|
var sw = document.getElementById('billingSwitch');
|
|
var saveTag = document.getElementById('saveTag');
|
|
var optM = document.getElementById('optMonthly');
|
|
var optY = document.getElementById('optYearly');
|
|
if (sw) {
|
|
sw.addEventListener('click', function () {
|
|
var yearly = sw.getAttribute('aria-checked') !== 'true';
|
|
sw.setAttribute('aria-checked', String(yearly));
|
|
if (saveTag) saveTag.classList.toggle('show', yearly);
|
|
if (optM) optM.classList.toggle('active', !yearly);
|
|
if (optY) optY.classList.toggle('active', yearly);
|
|
document.querySelectorAll('.price-val[data-monthly]').forEach(function (n) {
|
|
n.textContent = yearly ? n.getAttribute('data-yearly') : n.getAttribute('data-monthly');
|
|
});
|
|
document.querySelectorAll('.price-per[data-monthly]').forEach(function (n) {
|
|
n.textContent = yearly ? n.getAttribute('data-yearly') : n.getAttribute('data-monthly');
|
|
});
|
|
});
|
|
}
|
|
document.querySelectorAll('.faq-item').forEach(function (item) {
|
|
var btn = item.querySelector('.faq-q');
|
|
if (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var open = item.classList.toggle('open');
|
|
btn.setAttribute('aria-expanded', String(open));
|
|
});
|
|
}
|
|
});
|
|
})();
|
|
|
|
/* GSAP Animations & Interactions */
|
|
(function animations() {
|
|
if (!HAS_GSAP || REDUCED) {
|
|
document.querySelectorAll('[data-count]').forEach(function (n) {
|
|
n.textContent = n.getAttribute('data-count');
|
|
});
|
|
return;
|
|
}
|
|
var heroEls = document.querySelectorAll('[data-hero]');
|
|
gsap.set(heroEls, { opacity: 0, y: 20 });
|
|
var tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
|
|
heroEls.forEach(function (elh) {
|
|
var idx = parseInt(elh.getAttribute('data-hero'), 10);
|
|
tl.to(elh, { opacity: 1, y: 0, duration: 0.7 }, idx * 0.07 + 0.1);
|
|
});
|
|
var frame = document.querySelector('.qr-frame');
|
|
if (frame) gsap.from(frame, { rotation: -1.5, scale: 0.96, opacity: 0, duration: 0.9, ease: 'back.out(1.3)' }, 0.35);
|
|
|
|
if (HAS_ST) {
|
|
gsap.utils.toArray('[data-reveal]').forEach(function (elr) {
|
|
gsap.from(elr, {
|
|
opacity: 0, y: 24, duration: 0.7, ease: 'power3.out',
|
|
scrollTrigger: { trigger: elr, start: 'top 88%', once: true }
|
|
});
|
|
});
|
|
document.querySelectorAll('[data-count]').forEach(function (elc) {
|
|
var end = parseFloat(elc.getAttribute('data-count'));
|
|
var decimals = parseInt(elc.getAttribute('data-decimals') || '0', 10);
|
|
var obj = { v: 0 };
|
|
ScrollTrigger.create({
|
|
trigger: elc, start: 'top 92%', once: true,
|
|
onEnter: function () {
|
|
gsap.to(obj, {
|
|
v: end, duration: 1.3, ease: 'power2.out',
|
|
onUpdate: function () { elc.textContent = obj.v.toFixed(decimals); }
|
|
});
|
|
}
|
|
});
|
|
});
|
|
['generator', 'tools', 'pricing', 'faq'].forEach(function (id) {
|
|
var section = document.getElementById(id);
|
|
var link = document.querySelector('[data-nav="' + id + '"]');
|
|
if (!section || !link) return;
|
|
ScrollTrigger.create({
|
|
trigger: section, start: 'top 45%', end: 'bottom 45%',
|
|
onToggle: function (self) { link.classList.toggle('active', self.isActive); }
|
|
});
|
|
});
|
|
}
|
|
|
|
var marquee = document.getElementById('marqueeTrack');
|
|
if (marquee) {
|
|
var tween = gsap.to(marquee, { xPercent: -50, ease: 'none', duration: 100, repeat: -1 });
|
|
marquee.addEventListener('mouseenter', function () { gsap.to(tween, { timeScale: 0.08, duration: 0.4 }); });
|
|
marquee.addEventListener('mouseleave', function () { gsap.to(tween, { timeScale: 1, duration: 0.4 }); });
|
|
marquee.addEventListener('focusin', function () { gsap.to(tween, { timeScale: 0.08, duration: 0.4 }); });
|
|
marquee.addEventListener('focusout', function () { gsap.to(tween, { timeScale: 1, duration: 0.4 }); });
|
|
}
|
|
|
|
if (FINE_POINTER) {
|
|
document.querySelectorAll('[data-magnetic]').forEach(function (btn) {
|
|
btn.addEventListener('mousemove', function (e) {
|
|
var r = btn.getBoundingClientRect();
|
|
gsap.to(btn, {
|
|
x: (e.clientX - r.left - r.width / 2) * 0.15,
|
|
y: (e.clientY - r.top - r.height / 2) * 0.15,
|
|
duration: 0.3, ease: 'power3.out'
|
|
});
|
|
});
|
|
btn.addEventListener('mouseleave', function () {
|
|
gsap.to(btn, { x: 0, y: 0, duration: 0.4, ease: 'elastic.out(1, 0.4)' });
|
|
});
|
|
});
|
|
}
|
|
})();
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|