863 lines
48 KiB
HTML
863 lines
48 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<script>
|
||
(function(){var t=localStorage.getItem('qrknit-theme');if(t&&t!=='default')document.documentElement.setAttribute('data-theme',t);})();
|
||
</script>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>QRknit Private Instance — Self-Hosted URL Shortener & QR Generator</title>
|
||
<meta name="description" content="Run QRknit on your own infrastructure. Full data sovereignty, no third-party tracking, and complete control — available on Pro and Team plans. Deploy in minutes with Docker.">
|
||
<meta name="keywords" content="self-hosted URL shortener, private QR code generator, on-premise link management, Docker URL shortener, self-hosted link analytics">
|
||
<meta name="author" content="QRknit">
|
||
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
|
||
<link rel="canonical" href="https://qrknit.com/landing-os">
|
||
|
||
<!-- Open Graph / Facebook -->
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:url" content="https://qrknit.com/landing-os">
|
||
<meta property="og:site_name" content="QRknit">
|
||
<meta property="og:title" content="QRknit Private Instance — Self-Hosted URL Shortener & QR Generator">
|
||
<meta property="og:description" content="Run QRknit on your own infrastructure with full data sovereignty. Private deployment, no third-party tracking, complete control. Available on Pro and Team plans.">
|
||
<meta property="og:image" content="https://qrknit.com/static/og-image.png">
|
||
<meta property="og:image:width" content="1200">
|
||
<meta property="og:image:height" content="630">
|
||
<meta property="og:image:alt" content="QRknit Private Instance — Self-Hosted URL Shortener">
|
||
<meta property="og:locale" content="en_US">
|
||
|
||
<!-- Twitter / X Card -->
|
||
<meta name="twitter:card" content="summary_large_image">
|
||
<meta name="twitter:site" content="@qrknit">
|
||
<meta name="twitter:creator" content="@qrknit">
|
||
<meta name="twitter:title" content="QRknit Private Instance — Self-Hosted URL Shortener & QR Generator">
|
||
<meta name="twitter:description" content="Run QRknit on your own infrastructure with full data sovereignty. Private deployment, no third-party tracking, complete control.">
|
||
<meta name="twitter:image" content="https://qrknit.com/static/og-image.png">
|
||
<meta name="twitter:image:alt" content="QRknit Private Instance — Self-Hosted URL Shortener">
|
||
|
||
<!-- Favicon -->
|
||
<link rel="icon" type="image/png" href="/static/qk-ico.png">
|
||
<link rel="apple-touch-icon" href="/static/qk-ico.png">
|
||
|
||
<!-- JSON-LD Structured Data -->
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@graph": [
|
||
{
|
||
"@type": "WebPage",
|
||
"@id": "https://qrknit.com/landing-os#webpage",
|
||
"url": "https://qrknit.com/landing-os",
|
||
"name": "QRknit Private Instance — Self-Hosted URL Shortener & QR Generator",
|
||
"description": "Run QRknit on your own infrastructure with full data sovereignty. Private deployment available on Pro and Team plans.",
|
||
"isPartOf": { "@id": "https://qrknit.com/#website" },
|
||
"inLanguage": "en-US",
|
||
"breadcrumb": {
|
||
"@type": "BreadcrumbList",
|
||
"itemListElement": [
|
||
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://qrknit.com/" },
|
||
{ "@type": "ListItem", "position": 2, "name": "Private Instance", "item": "https://qrknit.com/landing-os" }
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"@type": "SoftwareApplication",
|
||
"name": "QRknit Private Instance",
|
||
"applicationCategory": "BusinessApplication",
|
||
"operatingSystem": "Linux, Docker",
|
||
"description": "Self-hosted, private deployment of QRknit — URL shortener with QR code generation and click analytics. Full data sovereignty with Docker-based deployment.",
|
||
"url": "https://qrknit.com/landing-os",
|
||
"offers": [
|
||
{
|
||
"@type": "Offer",
|
||
"name": "Pro Plan with Private Instance",
|
||
"price": "39",
|
||
"priceCurrency": "USD"
|
||
},
|
||
{
|
||
"@type": "Offer",
|
||
"name": "Team Plan with Private Instance",
|
||
"price": "79",
|
||
"priceCurrency": "USD"
|
||
}
|
||
],
|
||
"featureList": [
|
||
"Self-hosted deployment via Docker",
|
||
"Full data sovereignty",
|
||
"No third-party data sharing",
|
||
"Complete infrastructure control",
|
||
"All QRknit core features included",
|
||
"Private link shortening",
|
||
"Private QR code generation",
|
||
"Click analytics on your own servers"
|
||
]
|
||
}
|
||
]
|
||
}
|
||
</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=Orbitron:wght@400;700;900&family=Share+Tech+Mono&family=Barlow:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
:root {
|
||
--bg: #0a0a0f;
|
||
--surface: #111118;
|
||
--surface2: #181824;
|
||
--surface3: #1e1e2e;
|
||
--border: rgba(255,255,255,0.07);
|
||
--accent: #00b8d4;
|
||
--accent-rgb: 0,184,212;
|
||
--accent-grad: #1a52c8;
|
||
--accent-grad-rgb: 26,82,200;
|
||
--accent2: #ff5e62;
|
||
--accent3: #7c6af7;
|
||
--accent3-rgb: 124,106,247;
|
||
--accent3-light: #a89cf7;
|
||
--text: #f0f0f8;
|
||
--muted: #808098;
|
||
--radius: 12px;
|
||
--font-head: 'Orbitron', sans-serif;
|
||
--font-mono: 'Share Tech Mono', monospace;
|
||
--font-body: 'Barlow', sans-serif;
|
||
}
|
||
[data-theme="gold"] { --accent:#f5a623; --accent-rgb:245,166,35; --accent-grad:#c47f17; --accent-grad-rgb:196,127,23; --accent3:#d4a847; --accent3-rgb:212,168,71; --accent3-light:#e8c96a; }
|
||
[data-theme="green"] { --accent:#00c896; --accent-rgb:0,200,150; --accent-grad:#007a58; --accent-grad-rgb:0,122,88; --accent3:#4caf7a; --accent3-rgb:76,175,122; --accent3-light:#7fd9a4; }
|
||
[data-theme="red"] { --accent:#ff4757; --accent-rgb:255,71,87; --accent-grad:#c0392b; --accent-grad-rgb:192,57,43; --accent3:#ff6b7a; --accent3-rgb:255,107,122; --accent3-light:#ff9daa; }
|
||
|
||
html { scroll-behavior: smooth; }
|
||
body { background:var(--bg); color:var(--text); font-family:var(--font-body); font-size:15px; line-height:1.6; min-height:100vh; overflow-x:hidden; }
|
||
body::before {
|
||
content:''; position:fixed; inset:0; pointer-events:none; z-index:0;
|
||
background:
|
||
radial-gradient(ellipse 90% 60% at 15% -5%, rgba(var(--accent-rgb),.09) 0%, transparent 60%),
|
||
radial-gradient(ellipse 60% 50% at 90% 90%, rgba(var(--accent-grad-rgb),.08) 0%, transparent 55%),
|
||
radial-gradient(ellipse 50% 50% at 50% 50%, rgba(var(--accent-rgb),.03) 0%, transparent 60%);
|
||
}
|
||
|
||
/* ── HEADER ── */
|
||
header {
|
||
position:sticky; top:0; z-index:200;
|
||
display:flex; align-items:center; justify-content:space-between;
|
||
padding:0 40px; height:64px;
|
||
background:rgba(10,10,15,.88); backdrop-filter:blur(20px);
|
||
border-bottom:1px solid var(--border);
|
||
}
|
||
.logo { font-family:var(--font-head); font-size:18px; font-weight:700; letter-spacing:.04em; text-decoration:none; color:var(--text); display:inline-flex; align-items:center; gap:8px; }
|
||
.logo-dot-text { color:var(--accent); text-shadow:0 0 12px var(--accent); }
|
||
.logo-alwisp { text-transform:uppercase; }
|
||
.logo-icon-wrap { position:relative; display:inline-flex; align-items:center; flex-shrink:0; isolation:isolate; }
|
||
.logo-icon { height:26px; width:26px; object-fit:contain; border-radius:3px; display:block; }
|
||
.logo-icon-tint { position:absolute; inset:0; background:var(--accent); mix-blend-mode:hue; border-radius:3px; pointer-events:none; }
|
||
|
||
.header-right { display:flex; align-items:center; gap:12px; }
|
||
.theme-picker { display:flex; align-items:center; gap:6px; padding:0 4px; }
|
||
.theme-swatch { width:14px; height:14px; border-radius:50%; border:2px solid transparent; cursor:pointer; padding:0; transition:transform .2s,border-color .2s; }
|
||
.theme-swatch:hover { transform:scale(1.2); }
|
||
.theme-swatch.active { border-color:var(--text); transform:scale(1.15); }
|
||
.theme-swatch[data-theme="default"] { background:linear-gradient(135deg,#00b8d4,#1a52c8); }
|
||
.theme-swatch[data-theme="gold"] { background:linear-gradient(135deg,#f5a623,#c47f17); }
|
||
.theme-swatch[data-theme="green"] { background:linear-gradient(135deg,#00c896,#007a58); }
|
||
.theme-swatch[data-theme="red"] { background:linear-gradient(135deg,#ff4757,#c0392b); }
|
||
|
||
/* ── BUTTONS ── */
|
||
.btn-primary { background:linear-gradient(135deg,var(--accent),var(--accent-grad)); color:#fff; border:none; cursor:pointer; font-family:var(--font-head); font-weight:700; font-size:12px; letter-spacing:.04em; padding:12px 22px; border-radius:var(--radius); transition:all .2s; white-space:nowrap; text-decoration:none; display:inline-flex; align-items:center; gap:6px; }
|
||
.btn-primary:hover { transform:translateY(-1px); box-shadow:0 8px 24px rgba(var(--accent-rgb),.35); }
|
||
.btn-outline { background:transparent; border:1px solid var(--border); color:var(--text); font-family:var(--font-mono); font-size:12px; padding:11px 20px; border-radius:var(--radius); cursor:pointer; transition:all .2s; text-decoration:none; display:inline-flex; align-items:center; gap:6px; white-space:nowrap; }
|
||
.btn-outline:hover { border-color:rgba(var(--accent-rgb),.5); color:var(--accent); }
|
||
|
||
/* ── SECTIONS ── */
|
||
section { position:relative; z-index:1; }
|
||
.section-inner { max-width:1080px; margin:0 auto; padding:0 40px; }
|
||
|
||
/* ── HERO ── */
|
||
.hero { text-align:center; padding:110px 40px 80px; position:relative; z-index:1; overflow:hidden; }
|
||
.hero-eyebrow { font-family:var(--font-mono); font-size:11px; letter-spacing:.16em; color:var(--accent); text-transform:uppercase; margin-bottom:24px; }
|
||
.hero h1 {
|
||
font-family:var(--font-head); font-weight:900; line-height:1.05;
|
||
font-size:clamp(38px,7vw,82px); letter-spacing:.04em; margin-bottom:28px;
|
||
}
|
||
.hero-accent { background:linear-gradient(135deg,var(--accent) 0%,var(--accent-grad) 100%); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
|
||
.hero-sub { font-size:clamp(15px,2vw,19px); color:var(--muted); max-width:600px; margin:0 auto 44px; font-weight:300; line-height:1.7; }
|
||
.hero-cta { display:flex; align-items:center; justify-content:center; gap:14px; flex-wrap:wrap; margin-bottom:70px; }
|
||
.hero-note { font-family:var(--font-mono); font-size:11px; color:var(--muted); letter-spacing:.06em; }
|
||
.hero-note span { color:var(--accent); }
|
||
|
||
/* hero glow ring */
|
||
.hero::after {
|
||
content:''; position:absolute; left:50%; top:60px; transform:translateX(-50%);
|
||
width:600px; height:600px; border-radius:50%;
|
||
background:radial-gradient(ellipse, rgba(var(--accent-rgb),.07) 0%, transparent 70%);
|
||
pointer-events:none; z-index:-1;
|
||
}
|
||
|
||
/* ── FEATURE GRID ── */
|
||
.feature-section { padding:80px 0 90px; }
|
||
.section-label { font-family:var(--font-mono); font-size:10px; letter-spacing:.18em; color:var(--accent); text-transform:uppercase; margin-bottom:12px; text-align:center; }
|
||
.section-title { font-family:var(--font-head); font-size:clamp(22px,3vw,32px); font-weight:700; text-align:center; margin-bottom:56px; }
|
||
.feature-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:18px; }
|
||
.feature-card {
|
||
background:var(--surface); border:1px solid var(--border); border-radius:16px;
|
||
padding:28px; position:relative; overflow:hidden; transition:border-color .25s, transform .25s;
|
||
}
|
||
.feature-card::before {
|
||
content:''; position:absolute; top:0; left:0; right:0; height:2px;
|
||
background:linear-gradient(90deg,var(--accent),transparent);
|
||
}
|
||
.feature-card:hover { border-color:rgba(var(--accent-rgb),.25); transform:translateY(-3px); }
|
||
.feature-icon { width:40px; height:40px; border-radius:10px; background:rgba(var(--accent-rgb),.1); border:1px solid rgba(var(--accent-rgb),.2); display:flex; align-items:center; justify-content:center; margin-bottom:18px; flex-shrink:0; }
|
||
.feature-icon svg { color:var(--accent); }
|
||
.feature-card h3 { font-family:var(--font-head); font-size:13px; font-weight:700; letter-spacing:.04em; margin-bottom:10px; }
|
||
.feature-card p { font-size:13px; color:var(--muted); line-height:1.65; }
|
||
.feature-tag { display:inline-block; font-family:var(--font-mono); font-size:9px; letter-spacing:.08em; color:var(--accent3-light); background:rgba(var(--accent3-rgb),.1); border:1px solid rgba(var(--accent3-rgb),.2); border-radius:4px; padding:1px 7px; margin-top:14px; }
|
||
|
||
/* ── ANALYTICS SHOWCASE ── */
|
||
.analytics-section { padding:80px 0 90px; background:linear-gradient(180deg, transparent 0%, rgba(var(--accent-rgb),.03) 40%, transparent 100%); }
|
||
.analytics-split { display:grid; grid-template-columns:1fr 1fr; gap:60px; align-items:center; }
|
||
.analytics-copy .section-label { text-align:left; }
|
||
.analytics-copy .section-title { text-align:left; margin-bottom:20px; }
|
||
.analytics-copy p { color:var(--muted); font-size:14px; line-height:1.75; margin-bottom:16px; }
|
||
.analytics-list { list-style:none; margin:24px 0 32px; display:flex; flex-direction:column; gap:10px; }
|
||
.analytics-list li { font-family:var(--font-mono); font-size:12px; color:var(--text); display:flex; align-items:center; gap:10px; }
|
||
.analytics-list li::before { content:''; width:6px; height:6px; border-radius:50%; background:var(--accent); flex-shrink:0; }
|
||
|
||
/* mock analytics panel */
|
||
.mock-panel {
|
||
background:var(--surface); border:1px solid var(--border); border-radius:16px;
|
||
padding:20px; box-shadow:0 32px 80px rgba(0,0,0,.5);
|
||
}
|
||
.mock-panel-title { font-family:var(--font-mono); font-size:10px; color:var(--muted); letter-spacing:.1em; margin-bottom:14px; }
|
||
.mock-chart { display:flex; align-items:flex-end; gap:3px; height:70px; margin-bottom:16px; }
|
||
.mock-bar { border-radius:2px 2px 0 0; flex:1; min-width:4px; transition:opacity .3s; }
|
||
.mock-heatmap { display:grid; grid-template-columns:24px repeat(12,1fr); gap:2px; margin-bottom:14px; }
|
||
.mock-hm-label { font-family:var(--font-mono); font-size:7px; color:var(--muted); display:flex; align-items:center; }
|
||
.mock-hm-cell { height:10px; border-radius:2px; }
|
||
.mock-stats { display:grid; grid-template-columns:repeat(3,1fr); gap:8px; }
|
||
.mock-stat { background:var(--surface2); border:1px solid var(--border); border-radius:8px; padding:10px; }
|
||
.mock-stat-val { font-family:var(--font-head); font-size:16px; font-weight:700; color:var(--accent); line-height:1; margin-bottom:3px; }
|
||
.mock-stat-lbl { font-family:var(--font-mono); font-size:8px; color:var(--muted); letter-spacing:.06em; }
|
||
|
||
/* ── SELF-HOSTED ── */
|
||
.self-hosted-section { padding:80px 0; }
|
||
.pillar-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:20px; margin-top:56px; }
|
||
.pillar-card {
|
||
background:var(--surface2); border:1px solid var(--border); border-radius:14px;
|
||
padding:28px 24px; text-align:center;
|
||
}
|
||
.pillar-num { font-family:var(--font-head); font-size:36px; font-weight:900; line-height:1; background:linear-gradient(135deg,var(--accent),var(--accent-grad)); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; margin-bottom:8px; }
|
||
.pillar-label{ font-family:var(--font-mono); font-size:10px; color:var(--muted); letter-spacing:.1em; text-transform:uppercase; margin-bottom:10px; }
|
||
.pillar-desc { font-size:13px; color:var(--muted); line-height:1.6; }
|
||
|
||
/* ── DEPLOY ── */
|
||
.deploy-section { padding:80px 0 90px; }
|
||
.deploy-split { display:grid; grid-template-columns:1fr 1fr; gap:60px; align-items:start; }
|
||
.code-block {
|
||
background:var(--surface2); border:1px solid var(--border); border-radius:14px;
|
||
padding:24px; position:relative; overflow:hidden;
|
||
}
|
||
.code-block::before { content:''; position:absolute; top:0; left:0; right:0; height:1px; background:linear-gradient(90deg,var(--accent),transparent); }
|
||
.code-block pre { font-family:var(--font-mono); font-size:12px; color:var(--text); line-height:1.8; overflow-x:auto; white-space:pre; }
|
||
.code-line { display:block; }
|
||
.code-key { color:var(--accent3-light); }
|
||
.code-val { color:rgba(var(--accent-rgb),.9); }
|
||
.code-flag { color:var(--muted); }
|
||
.deploy-copy .section-title { text-align:left; margin-bottom:18px; }
|
||
.deploy-copy .section-label { text-align:left; }
|
||
.deploy-copy p { color:var(--muted); font-size:14px; line-height:1.75; margin-bottom:16px; }
|
||
.deploy-steps { list-style:none; margin:24px 0 0; display:flex; flex-direction:column; gap:12px; counter-reset:step; }
|
||
.deploy-steps li { font-size:13px; color:var(--muted); display:flex; align-items:flex-start; gap:12px; counter-increment:step; }
|
||
.deploy-steps li::before {
|
||
content:counter(step); flex-shrink:0;
|
||
width:22px; height:22px; border-radius:50%;
|
||
background:rgba(var(--accent-rgb),.1); border:1px solid rgba(var(--accent-rgb),.25);
|
||
font-family:var(--font-mono); font-size:10px; color:var(--accent);
|
||
display:flex; align-items:center; justify-content:center; margin-top:1px;
|
||
}
|
||
|
||
/* ── PRICING ── */
|
||
.pricing-section { padding:80px 0 90px; }
|
||
.pricing-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:20px; margin-top:56px; }
|
||
.pricing-card {
|
||
background:var(--surface); border:1px solid var(--border); border-radius:16px;
|
||
padding:32px 28px; display:flex; flex-direction:column; position:relative; overflow:hidden;
|
||
}
|
||
.pricing-card.featured {
|
||
border-color:rgba(var(--accent-rgb),.35);
|
||
background:linear-gradient(160deg, var(--surface2) 0%, var(--surface) 100%);
|
||
box-shadow:0 0 0 1px rgba(var(--accent-rgb),.15), 0 24px 60px rgba(0,0,0,.4);
|
||
}
|
||
.pricing-card.featured::before { content:''; position:absolute; top:0; left:0; right:0; height:2px; background:linear-gradient(90deg,var(--accent),var(--accent-grad)); }
|
||
.pricing-badge { position:absolute; top:18px; right:18px; font-family:var(--font-mono); font-size:9px; letter-spacing:.1em; text-transform:uppercase; background:rgba(var(--accent-rgb),.15); border:1px solid rgba(var(--accent-rgb),.3); color:var(--accent); border-radius:4px; padding:2px 8px; }
|
||
.pricing-name { font-family:var(--font-head); font-size:13px; font-weight:700; letter-spacing:.06em; margin-bottom:6px; text-transform:uppercase; }
|
||
.pricing-price { font-family:var(--font-head); font-size:36px; font-weight:900; line-height:1; margin:14px 0 6px; }
|
||
.pricing-price span { font-size:14px; color:var(--muted); font-family:var(--font-body); font-weight:300; }
|
||
.pricing-desc { font-size:13px; color:var(--muted); margin-bottom:24px; line-height:1.6; }
|
||
.pricing-features { list-style:none; margin:0 0 28px; display:flex; flex-direction:column; gap:9px; flex:1; }
|
||
.pricing-features li { font-size:13px; color:var(--muted); display:flex; align-items:flex-start; gap:8px; }
|
||
.pricing-features li::before { content:'✓'; color:var(--accent); font-family:var(--font-mono); font-size:11px; flex-shrink:0; margin-top:2px; }
|
||
.pricing-features li.dim { opacity:.45; }
|
||
.pricing-features li.dim::before { content:'—'; color:var(--muted); }
|
||
.pricing-soon { background:var(--surface2); border:1px solid var(--border); color:var(--muted); font-family:var(--font-mono); font-size:11px; padding:10px; border-radius:8px; text-align:center; margin-top:auto; letter-spacing:.04em; cursor:pointer; transition:all .2s; }
|
||
.pricing-soon:hover { border-color:rgba(var(--accent-rgb),.5); color:var(--accent); }
|
||
|
||
/* ── FINAL CTA ── */
|
||
.cta-section { padding:80px 40px 100px; text-align:center; position:relative; z-index:1; }
|
||
.cta-section h2 { font-family:var(--font-head); font-size:clamp(24px,4vw,42px); font-weight:900; margin-bottom:16px; }
|
||
.cta-section p { font-size:15px; color:var(--muted); max-width:480px; margin:0 auto 36px; }
|
||
.cta-sub { font-family:var(--font-mono); font-size:11px; color:var(--muted); margin-top:20px; letter-spacing:.06em; }
|
||
|
||
/* ── FOOTER ── */
|
||
footer {
|
||
border-top:1px solid var(--border); padding:32px 40px;
|
||
display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:16px;
|
||
position:relative; z-index:1;
|
||
}
|
||
.footer-logo { font-family:var(--font-head); font-size:14px; font-weight:700; color:var(--muted); letter-spacing:.04em; display:flex; align-items:center; gap:6px; }
|
||
.footer-logo span { color:var(--accent); }
|
||
.footer-links { display:flex; gap:20px; align-items:center; }
|
||
.footer-links a { font-family:var(--font-mono); font-size:11px; color:var(--muted); text-decoration:none; letter-spacing:.06em; transition:color .2s; }
|
||
.footer-links a:hover { color:var(--accent); }
|
||
.footer-note { font-family:var(--font-mono); font-size:10px; color:var(--muted); opacity:.5; letter-spacing:.05em; }
|
||
|
||
/* ── DIVIDER ── */
|
||
.section-divider { border:none; border-top:1px solid var(--border); margin:0; }
|
||
|
||
/* ── ERROR BANNER ── */
|
||
.error-banner { display:none; position:fixed; top:72px; left:50%; transform:translateX(-50%); z-index:300; background:var(--surface2); border:1px solid rgba(255,94,98,.3); border-radius:10px; padding:12px 20px; font-family:var(--font-mono); font-size:12px; color:#ff5e62; box-shadow:0 8px 24px rgba(0,0,0,.4); white-space:nowrap; }
|
||
.error-banner.show { display:block; animation:fadeDown .3s ease; }
|
||
@keyframes fadeDown { from{opacity:0;transform:translateX(-50%) translateY(-8px)} to{opacity:1;transform:translateX(-50%) translateY(0)} }
|
||
|
||
/* ── REVEAL ANIMATIONS ── */
|
||
.reveal { opacity:0; transform:translateY(24px); transition:opacity .55s ease, transform .55s ease; }
|
||
.reveal.visible { opacity:1; transform:none; }
|
||
.reveal-delay-1 { transition-delay:.1s; }
|
||
.reveal-delay-2 { transition-delay:.2s; }
|
||
.reveal-delay-3 { transition-delay:.3s; }
|
||
.reveal-delay-4 { transition-delay:.4s; }
|
||
.reveal-delay-5 { transition-delay:.5s; }
|
||
|
||
/* ── RESPONSIVE ── */
|
||
@media(max-width:900px) {
|
||
header { padding:0 20px; }
|
||
.section-inner { padding:0 20px; }
|
||
.hero { padding:80px 20px 60px; }
|
||
.feature-grid, .pillar-grid, .pricing-grid { grid-template-columns:1fr 1fr; }
|
||
.analytics-split, .deploy-split { grid-template-columns:1fr; gap:36px; }
|
||
footer { padding:24px 20px; }
|
||
}
|
||
@media(max-width:600px) {
|
||
.feature-grid, .pillar-grid, .pricing-grid { grid-template-columns:1fr; }
|
||
.hero-cta { flex-direction:column; align-items:center; }
|
||
}
|
||
|
||
/* ── CONTACT MODAL ── */
|
||
.contact-backdrop { display:none; position:fixed; inset:0; z-index:600; background:rgba(0,0,0,.65); backdrop-filter:blur(4px); align-items:center; justify-content:center; padding:20px; }
|
||
.contact-backdrop.open { display:flex; }
|
||
.contact-modal { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:36px; width:100%; max-width:480px; position:relative; box-shadow:0 24px 60px rgba(0,0,0,.5); }
|
||
.contact-modal h3 { font-family:var(--font-head); font-size:16px; font-weight:700; letter-spacing:.04em; margin-bottom:6px; }
|
||
.contact-modal .cm-sub { font-size:13px; color:var(--muted); margin-bottom:24px; line-height:1.5; }
|
||
.contact-modal label { display:block; font-family:var(--font-mono); font-size:11px; letter-spacing:.06em; color:var(--muted); margin-bottom:6px; }
|
||
.contact-modal input, .contact-modal textarea { width:100%; background:var(--surface2); border:1px solid var(--border); border-radius:8px; padding:10px 14px; color:var(--text); font-family:var(--font-body); font-size:14px; outline:none; transition:border-color .2s; }
|
||
.contact-modal input:focus, .contact-modal textarea:focus { border-color:rgba(var(--accent-rgb),.5); }
|
||
.contact-modal textarea { resize:vertical; min-height:80px; }
|
||
.contact-field { margin-bottom:16px; }
|
||
.cm-actions { display:flex; gap:12px; justify-content:flex-end; margin-top:24px; }
|
||
.cm-close { position:absolute; top:14px; right:16px; background:none; border:none; color:var(--muted); font-size:22px; cursor:pointer; line-height:1; padding:4px; }
|
||
.cm-close:hover { color:var(--text); }
|
||
.cm-success { display:none; text-align:center; padding:20px 0; }
|
||
.cm-success .cm-check { font-size:40px; color:var(--accent); margin-bottom:12px; }
|
||
.cm-success p { color:var(--muted); font-size:14px; margin-top:8px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="error-banner" id="error-banner"></div>
|
||
|
||
<!-- ── HEADER ── -->
|
||
<header>
|
||
<a class="logo" href="/">
|
||
<span class="logo-icon-wrap"><img class="logo-icon" src="/static/qk-ico.png" alt=""><span class="logo-icon-tint"></span></span>QR<span class="logo-dot-text">knit</span>
|
||
</a>
|
||
<div class="header-right">
|
||
<div class="theme-picker">
|
||
<button class="theme-swatch" data-theme="default" title="Default" aria-label="Default blue theme"></button>
|
||
<button class="theme-swatch" data-theme="gold" title="Gold" aria-label="Gold theme"></button>
|
||
<button class="theme-swatch" data-theme="green" title="Green" aria-label="Green theme"></button>
|
||
<button class="theme-swatch" data-theme="red" title="Red" aria-label="Red theme"></button>
|
||
</div>
|
||
<a class="btn-primary" href="/app">Open App →</a>
|
||
</div>
|
||
</header>
|
||
|
||
|
||
<!-- ── HERO ── -->
|
||
<section class="hero">
|
||
<div class="hero-eyebrow reveal">Private Instance · Pro & Team plans</div>
|
||
<h1 class="reveal reveal-delay-1">Short links.<br><span class="hero-accent">Sharp QR codes.</span><br>Your infrastructure.</h1>
|
||
<p class="hero-sub reveal reveal-delay-2">
|
||
All the power of QRknit running on your own hardware, under your full control. Your data stays on your server — no sharing, no usage caps from noisy neighbours.
|
||
</p>
|
||
<div class="hero-cta reveal reveal-delay-3">
|
||
<a class="btn-primary" href="/app" style="font-size:13px;padding:14px 28px">→ Open App</a>
|
||
<a class="btn-outline" href="/" style="font-size:13px;padding:14px 28px">View plans →</a>
|
||
</div>
|
||
<div class="hero-note reveal reveal-delay-4">
|
||
<span>Single Docker container</span> · Included with Pro & Team plans
|
||
</div>
|
||
</section>
|
||
|
||
|
||
<!-- ── FEATURES ── -->
|
||
<section class="feature-section">
|
||
<div class="section-inner">
|
||
<div class="section-label reveal">What's inside</div>
|
||
<div class="section-title reveal reveal-delay-1">Everything in one container</div>
|
||
<div class="feature-grid">
|
||
|
||
<div class="feature-card reveal reveal-delay-1">
|
||
<div class="feature-icon">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||
</div>
|
||
<h3>Link Shortening</h3>
|
||
<p>Custom codes, automatic expiry dates, tag organisation, bulk import and export. Search and filter your entire link library in milliseconds.</p>
|
||
<span class="feature-tag">Custom codes · Tags · Expiry · Bulk CSV</span>
|
||
</div>
|
||
|
||
<div class="feature-card reveal reveal-delay-2">
|
||
<div class="feature-icon" style="background:rgba(var(--accent3-rgb),.1);border-color:rgba(var(--accent3-rgb),.2)">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent3-light)"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 7h.01M17 7h.01M7 17h.01M11 7h2M7 11v2M15 11h2M11 17h2M17 13v2"/></svg>
|
||
</div>
|
||
<h3>QR Code Generator</h3>
|
||
<p>Five dot styles, full color control, logo overlay support, and downloadable PNGs — right in the browser. Generate for any URL, not just your short links.</p>
|
||
<span class="feature-tag" style="color:var(--accent3-light);background:rgba(var(--accent3-rgb),.1);border-color:rgba(var(--accent3-rgb),.2)">5 styles · Logo overlay · Custom colors</span>
|
||
</div>
|
||
|
||
<div class="feature-card reveal reveal-delay-3">
|
||
<div class="feature-icon" style="background:rgba(245,166,35,.1);border-color:rgba(245,166,35,.2)">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:#f5a623"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
||
</div>
|
||
<h3>Click Analytics</h3>
|
||
<p>Daily bar charts, hourly 7×24 activity heatmap, geographic country breakdown, referrer and device tracking — all per link, with raw CSV export.</p>
|
||
<span class="feature-tag" style="color:#e8c96a;background:rgba(245,166,35,.1);border-color:rgba(245,166,35,.2)">Heatmap · Geo · Referrers · CSV export</span>
|
||
</div>
|
||
|
||
<div class="feature-card reveal reveal-delay-1">
|
||
<div class="feature-icon">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||
</div>
|
||
<h3>Multi-user Support</h3>
|
||
<p>Admin account management, per-user isolation so each user only sees their own links, and owner attribution so admins can quickly identify and act on abuse.</p>
|
||
<span class="feature-tag">Admin panel · User isolation · Owner badges</span>
|
||
</div>
|
||
|
||
<div class="feature-card reveal reveal-delay-2">
|
||
<div class="feature-icon" style="background:rgba(0,200,150,.1);border-color:rgba(0,200,150,.2)">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:#00c896"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
|
||
</div>
|
||
<h3>Docker Native</h3>
|
||
<p>One container, one volume, one command. SQLite keeps it simple — no Postgres, no Redis, no message queue. Works on Unraid, Synology, or any Docker host.</p>
|
||
<span class="feature-tag" style="color:#7fd9a4;background:rgba(0,200,150,.1);border-color:rgba(0,200,150,.2)">Single container · SQLite · Zero infra deps</span>
|
||
</div>
|
||
|
||
<div class="feature-card reveal reveal-delay-3">
|
||
<div class="feature-icon" style="background:rgba(255,71,87,.1);border-color:rgba(255,71,87,.2)">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:#ff4757"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
|
||
</div>
|
||
<h3>Four Color Themes</h3>
|
||
<p>Switch between Cyan, Gold, Green, and Red accent palettes. Preference is stored in localStorage and applies instantly — no page reload, no account needed.</p>
|
||
<span class="feature-tag" style="color:#ff9daa;background:rgba(255,71,87,.1);border-color:rgba(255,71,87,.2)">Cyan · Gold · Green · Red</span>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<!-- ── ANALYTICS SHOWCASE ── -->
|
||
<section class="analytics-section">
|
||
<div class="section-inner">
|
||
<div class="analytics-split">
|
||
<div class="analytics-copy">
|
||
<div class="section-label reveal">Analytics deep-dive</div>
|
||
<h2 class="section-title reveal reveal-delay-1">Know exactly who clicked, when, and from where</h2>
|
||
<p class="reveal reveal-delay-2">Most link shorteners give you a click count. QRknit gives you the full picture — every event captured and broken down so you can make real decisions.</p>
|
||
<ul class="analytics-list reveal reveal-delay-3">
|
||
<li>Daily click bar chart per link (7d · 30d · 90d windows)</li>
|
||
<li>Hourly 7×24 activity heatmap — spot your peak times at a glance</li>
|
||
<li>Geographic country breakdown via Cloudflare headers</li>
|
||
<li>Referrer, device type, and browser breakdown</li>
|
||
<li>30-day dashboard-wide click chart across all links</li>
|
||
<li>Raw click-event CSV export — every timestamp, referrer, country</li>
|
||
</ul>
|
||
<a class="btn-primary reveal reveal-delay-4" href="/app">See it in action →</a>
|
||
</div>
|
||
|
||
<!-- Mock analytics panel -->
|
||
<div class="mock-panel reveal reveal-delay-2" id="mock-panel">
|
||
<div class="mock-panel-title">CLICK ANALYTICS — LAST 30 DAYS</div>
|
||
<div class="mock-chart" id="mock-chart"></div>
|
||
<div class="mock-panel-title" style="margin-top:4px">HOURLY ACTIVITY</div>
|
||
<div class="mock-heatmap" id="mock-heatmap"></div>
|
||
<div class="mock-stats">
|
||
<div class="mock-stat"><div class="mock-stat-val" id="mock-total">—</div><div class="mock-stat-lbl">TOTAL CLICKS</div></div>
|
||
<div class="mock-stat"><div class="mock-stat-val" id="mock-peak">—</div><div class="mock-stat-lbl">PEAK DAY</div></div>
|
||
<div class="mock-stat"><div class="mock-stat-val" id="mock-countries">—</div><div class="mock-stat-lbl">COUNTRIES</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<!-- ── PRIVATE INSTANCE ── -->
|
||
<section class="self-hosted-section">
|
||
<div class="section-inner">
|
||
<div class="section-label reveal">Data sovereignty</div>
|
||
<div class="section-title reveal reveal-delay-1">Your data never leaves your server</div>
|
||
<div class="pillar-grid">
|
||
<div class="pillar-card reveal reveal-delay-1">
|
||
<div class="pillar-num">0</div>
|
||
<div class="pillar-label">External infra deps</div>
|
||
<p class="pillar-desc">No Redis, no Postgres, no message queue. A single SQLite file holds everything. Back it up with one <code style="font-family:var(--font-mono);font-size:11px;color:var(--accent)">cp</code>.</p>
|
||
</div>
|
||
<div class="pillar-card reveal reveal-delay-2">
|
||
<div class="pillar-num">1</div>
|
||
<div class="pillar-label">Docker container</div>
|
||
<p class="pillar-desc">The entire app — Python, Flask, frontend, and all assets — ships as a single image. Map one port, mount one volume, done.</p>
|
||
</div>
|
||
<div class="pillar-card reveal reveal-delay-3">
|
||
<div class="pillar-num">∞</div>
|
||
<div class="pillar-label">Links you can create</div>
|
||
<p class="pillar-desc">Your plan limits apply, but your instance is yours alone — no shared infrastructure, no noisy neighbours, no cross-tenant data.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<!-- ── DEPLOY ── -->
|
||
<section class="deploy-section">
|
||
<div class="section-inner">
|
||
<div class="deploy-split">
|
||
<div class="deploy-copy">
|
||
<div class="section-label reveal">Get running in minutes</div>
|
||
<h2 class="section-title reveal reveal-delay-1">One command to deploy</h2>
|
||
<p class="reveal reveal-delay-2">No CI pipeline, no cloud credentials, no YAML sprawl. Access the private repository with your plan credentials, build the image, run the container.</p>
|
||
<ol class="deploy-steps reveal reveal-delay-3">
|
||
<li>Clone the private repo using the credentials from your account</li>
|
||
<li>Set <code style="font-family:var(--font-mono);color:var(--accent);font-size:12px">BASE_URL</code>, <code style="font-family:var(--font-mono);color:var(--accent);font-size:12px">SECRET_KEY</code>, and <code style="font-family:var(--font-mono);color:var(--accent);font-size:12px">ADMIN_PASSWORD</code></li>
|
||
<li>Point a domain at your server and enable HTTPS</li>
|
||
<li>Log in at <code style="font-family:var(--font-mono);color:var(--accent);font-size:12px">/app</code> and start shortening</li>
|
||
</ol>
|
||
</div>
|
||
<div class="code-block reveal reveal-delay-2">
|
||
<pre><span class="code-line"><span class="code-flag">docker run -d \</span></span>
|
||
<span class="code-line"> <span class="code-key">--name</span> qrknit \</span>
|
||
<span class="code-line"> <span class="code-key">--restart</span> unless-stopped \</span>
|
||
<span class="code-line"> <span class="code-key">-p</span> 5000:5000 \</span>
|
||
<span class="code-line"> <span class="code-key">-v</span> qrknit-data:/app/data \</span>
|
||
<span class="code-line"> <span class="code-key">-e</span> <span class="code-val">BASE_URL</span>=https://yourdomain.com \</span>
|
||
<span class="code-line"> <span class="code-key">-e</span> <span class="code-val">SECRET_KEY</span>=your-generated-key \</span>
|
||
<span class="code-line"> <span class="code-key">-e</span> <span class="code-val">ADMIN_PASSWORD</span>=your-password \</span>
|
||
<span class="code-line"> qrknit:latest</span></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<!-- ── PRICING ── -->
|
||
<section class="pricing-section">
|
||
<div class="section-inner">
|
||
<div class="section-label reveal">Pricing</div>
|
||
<div class="section-title reveal reveal-delay-1">Private instance is included with Pro and Team plans.</div>
|
||
<div class="pricing-grid">
|
||
|
||
<div class="pricing-card featured reveal reveal-delay-1">
|
||
<div class="pricing-badge" style="background:rgba(var(--accent3-rgb),.12);border-color:rgba(var(--accent3-rgb),.3);color:var(--accent3-light)">Coming soon</div>
|
||
<div class="pricing-name">Pro</div>
|
||
<div class="pricing-price">$39 <span>/ month</span></div>
|
||
<p class="pricing-desc">For power users and growing teams who need full analytics and a private domain.</p>
|
||
<ul class="pricing-features">
|
||
<li>Unlimited active links</li>
|
||
<li>500,000 tracked clicks / month</li>
|
||
<li>90-day click analytics</li>
|
||
<li>Geographic country breakdown</li>
|
||
<li>Hourly 7×24 heatmap</li>
|
||
<li>1 custom short domain</li>
|
||
<li>Private instance</li>
|
||
<li>Up to 5 team members</li>
|
||
<li>Priority email support</li>
|
||
</ul>
|
||
<button class="pricing-soon" onclick="openContact('notify-me-pro')">Notify me when available</button>
|
||
</div>
|
||
|
||
<div class="pricing-card reveal reveal-delay-2">
|
||
<div class="pricing-badge" style="background:rgba(var(--accent3-rgb),.12);border-color:rgba(var(--accent3-rgb),.3);color:var(--accent3-light)">Coming soon</div>
|
||
<div class="pricing-name">Team</div>
|
||
<div class="pricing-price">$79 <span>/ month</span></div>
|
||
<p class="pricing-desc">Advanced features and full API access for teams that need more control.</p>
|
||
<ul class="pricing-features">
|
||
<li>Unlimited links & clicks</li>
|
||
<li>Everything in Pro</li>
|
||
<li>Up to 3 custom short domains</li>
|
||
<li>Private instance</li>
|
||
<li>Unlimited team members</li>
|
||
<li>Full REST API + API key auth</li>
|
||
<li>UTM parameter builder</li>
|
||
<li>Password-protected links</li>
|
||
<li>Dedicated account support</li>
|
||
</ul>
|
||
<button class="pricing-soon" onclick="openContact('notify-me-team')" style="border-color:rgba(var(--accent3-rgb),.2)">Notify me when available</button>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<!-- ── FINAL CTA ── -->
|
||
<section class="cta-section">
|
||
<div class="section-label reveal">Ready?</div>
|
||
<h2 class="reveal reveal-delay-1">Your links are waiting.</h2>
|
||
<p class="reveal reveal-delay-2">Open the app and start shortening URLs, generating QR codes, and tracking clicks — right now.</p>
|
||
<div class="reveal reveal-delay-3">
|
||
<a class="btn-primary" href="/app" style="font-size:13px;padding:14px 32px;display:inline-flex">→ Open App</a>
|
||
</div>
|
||
<div class="cta-sub reveal reveal-delay-4">Included with Pro & Team plans · Full data sovereignty · Your domain</div>
|
||
</section>
|
||
|
||
|
||
<!-- ── FOOTER ── -->
|
||
<footer>
|
||
<div class="footer-logo">
|
||
<span class="logo-icon-wrap" style="isolation:isolate;position:relative;display:inline-flex"><img style="height:20px;width:20px;object-fit:contain;border-radius:2px;display:block" src="/static/qk-ico.png" alt=""><span style="position:absolute;inset:0;background:var(--accent);mix-blend-mode:hue;border-radius:2px;pointer-events:none"></span></span>
|
||
QR<span>k</span>nit
|
||
</div>
|
||
<div class="footer-links">
|
||
<a href="/">Plans</a>
|
||
<a href="#" onclick="openContact('contact-sales');return false">Contact Sales</a>
|
||
<a href="/api-docs">API Docs</a>
|
||
<a href="/app">App</a>
|
||
</div>
|
||
<div class="footer-note">© 2025 QRknit · All rights reserved · <a href="/notices" style="color:inherit;text-decoration:none;opacity:.7">Third-party notices</a></div>
|
||
</footer>
|
||
|
||
|
||
<!-- ── Contact modal ── -->
|
||
<div class="contact-backdrop" id="contact-backdrop" onclick="contactBackdropClick(event)">
|
||
<div class="contact-modal">
|
||
<button class="cm-close" onclick="closeContact()" aria-label="Close">×</button>
|
||
<div id="cm-form-content">
|
||
<h3>Get in Touch</h3>
|
||
<p class="cm-sub" id="cm-subtitle">Send us a message and we'll get back to you.</p>
|
||
<div class="contact-field">
|
||
<label for="cm-name">NAME</label>
|
||
<input type="text" id="cm-name" placeholder="Your name" autocomplete="name">
|
||
</div>
|
||
<div class="contact-field">
|
||
<label for="cm-email">EMAIL</label>
|
||
<input type="email" id="cm-email" placeholder="you@example.com" autocomplete="email">
|
||
</div>
|
||
<div class="contact-field">
|
||
<label for="cm-body">MESSAGE <span style="font-size:10px;opacity:.6">(optional)</span></label>
|
||
<textarea id="cm-body" placeholder="Anything you'd like to add…"></textarea>
|
||
</div>
|
||
<input type="hidden" id="cm-subject">
|
||
<div class="cm-actions">
|
||
<button class="btn-outline" type="button" onclick="closeContact()">Cancel</button>
|
||
<button class="btn-primary" type="button" onclick="submitContact()" id="cm-submit">Send message</button>
|
||
</div>
|
||
</div>
|
||
<div class="cm-success" id="cm-success">
|
||
<div class="cm-check">✓</div>
|
||
<h3 style="margin-bottom:8px">Message sent!</h3>
|
||
<p>Thanks for reaching out. We'll be in touch soon.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ── Theme picker ──────────────────────────────────────────────────────────
|
||
function applyTheme(name) {
|
||
if (name && name !== 'default') {
|
||
document.documentElement.setAttribute('data-theme', name);
|
||
} else {
|
||
document.documentElement.removeAttribute('data-theme');
|
||
}
|
||
localStorage.setItem('qrknit-theme', name || 'default');
|
||
document.querySelectorAll('.theme-swatch').forEach(s => {
|
||
s.classList.toggle('active', s.dataset.theme === (name || 'default'));
|
||
});
|
||
}
|
||
document.querySelectorAll('.theme-swatch').forEach(s => {
|
||
s.addEventListener('click', () => applyTheme(s.dataset.theme));
|
||
});
|
||
// Restore active swatch on load
|
||
(function(){
|
||
const t = localStorage.getItem('qrknit-theme') || 'default';
|
||
document.querySelectorAll('.theme-swatch').forEach(s => {
|
||
s.classList.toggle('active', s.dataset.theme === t);
|
||
});
|
||
})();
|
||
|
||
// ── Error banner (for /?error= from expired/dead short links) ─────────────
|
||
(function(){
|
||
const p = new URLSearchParams(location.search);
|
||
const err = p.get('error');
|
||
const banner = document.getElementById('error-banner');
|
||
const msgs = { not_found: 'That short link does not exist.', expired: 'That short link has expired.' };
|
||
if (err && msgs[err]) {
|
||
banner.textContent = msgs[err];
|
||
banner.classList.add('show');
|
||
setTimeout(() => banner.classList.remove('show'), 5000);
|
||
}
|
||
})();
|
||
|
||
// ── Scroll reveal ─────────────────────────────────────────────────────────
|
||
(function(){
|
||
const obs = new IntersectionObserver((entries) => {
|
||
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('visible'); obs.unobserve(e.target); } });
|
||
}, { threshold: 0.12 });
|
||
document.querySelectorAll('.reveal').forEach(el => obs.observe(el));
|
||
// Make hero items visible immediately (they're above the fold)
|
||
document.querySelectorAll('.hero .reveal').forEach(el => el.classList.add('visible'));
|
||
})();
|
||
|
||
// ── Mock analytics panel ──────────────────────────────────────────────────
|
||
(function(){
|
||
const accentRgb = () => getComputedStyle(document.documentElement).getPropertyValue('--accent-rgb').trim() || '0,184,212';
|
||
|
||
// Generate plausible-looking demo data
|
||
const days = 30;
|
||
const data = Array.from({length: days}, (_, i) => {
|
||
const base = 20 + Math.sin(i / 4) * 12 + Math.random() * 18;
|
||
return Math.max(0, Math.round(base));
|
||
});
|
||
// Spike a couple of days
|
||
data[7] = 58; data[18] = 71; data[23] = 44;
|
||
const total = data.reduce((s, v) => s + v, 0);
|
||
const peak = Math.max(...data);
|
||
|
||
// Heatmap data (7 days × 12 two-hour buckets)
|
||
const hmap = Array.from({length: 7}, () =>
|
||
Array.from({length: 12}, (_, h) => {
|
||
const peak_h = (h >= 4 && h <= 8) ? 1.8 : (h >= 1 && h <= 3) ? 0.4 : 1;
|
||
return Math.round(Math.random() * 8 * peak_h);
|
||
})
|
||
);
|
||
const hmMax = Math.max(...hmap.flat(), 1);
|
||
|
||
function render() {
|
||
const rgb = accentRgb();
|
||
// Bar chart
|
||
const chartEl = document.getElementById('mock-chart');
|
||
if (!chartEl) return;
|
||
const maxV = Math.max(...data, 1);
|
||
chartEl.innerHTML = data.map(v => {
|
||
const pct = (v / maxV * 100).toFixed(0);
|
||
const op = v > 0 ? (0.2 + (v / maxV) * 0.7).toFixed(2) : '0.08';
|
||
return `<div class="mock-bar" style="height:${pct}%;background:rgba(${rgb},${op})"></div>`;
|
||
}).join('');
|
||
|
||
// Heatmap
|
||
const heatEl = document.getElementById('mock-heatmap');
|
||
if (!heatEl) return;
|
||
const days_lbl = ['M','T','W','T','F','S','S'];
|
||
let html = '';
|
||
hmap.forEach((row, di) => {
|
||
html += `<div class="mock-hm-label">${days_lbl[di]}</div>`;
|
||
row.forEach(v => {
|
||
const op = v > 0 ? (0.12 + (v / hmMax) * 0.82).toFixed(2) : '0.05';
|
||
html += `<div class="mock-hm-cell" style="background:rgba(${rgb},${op})"></div>`;
|
||
});
|
||
});
|
||
heatEl.innerHTML = html;
|
||
|
||
// Stats
|
||
document.getElementById('mock-total').textContent = total.toLocaleString();
|
||
document.getElementById('mock-peak').textContent = peak;
|
||
document.getElementById('mock-countries').textContent = '12';
|
||
}
|
||
|
||
render();
|
||
// Re-render when theme changes so colors update
|
||
document.querySelectorAll('.theme-swatch').forEach(s => {
|
||
s.addEventListener('click', () => setTimeout(render, 50));
|
||
});
|
||
})();
|
||
|
||
// ── Contact form ───────────────────────────────────────────────────────────
|
||
const CM_SUBTITLES = {
|
||
'request-access': "Interested in getting started? Leave your details and we'll follow up.",
|
||
'notify-me-cloud': "We'll let you know as soon as Cloud hosting is available.",
|
||
'notify-me-team': "We'll let you know when the Team tier launches.",
|
||
};
|
||
|
||
function openContact(subject) {
|
||
document.getElementById('cm-subject').value = subject;
|
||
const sub = CM_SUBTITLES[subject] || "Send us a message and we'll get back to you.";
|
||
document.getElementById('cm-subtitle').textContent = sub;
|
||
document.getElementById('cm-form-content').style.display = '';
|
||
document.getElementById('cm-success').style.display = 'none';
|
||
document.getElementById('cm-name').value = '';
|
||
document.getElementById('cm-email').value = '';
|
||
document.getElementById('cm-body').value = '';
|
||
const btn = document.getElementById('cm-submit');
|
||
btn.disabled = false;
|
||
btn.textContent = 'Send message';
|
||
document.getElementById('contact-backdrop').classList.add('open');
|
||
setTimeout(() => document.getElementById('cm-name').focus(), 80);
|
||
}
|
||
|
||
function closeContact() {
|
||
document.getElementById('contact-backdrop').classList.remove('open');
|
||
}
|
||
|
||
function contactBackdropClick(e) {
|
||
if (e.target === document.getElementById('contact-backdrop')) closeContact();
|
||
}
|
||
|
||
async function submitContact() {
|
||
const name = document.getElementById('cm-name').value.trim();
|
||
const email = document.getElementById('cm-email').value.trim();
|
||
const subject = document.getElementById('cm-subject').value;
|
||
const body = document.getElementById('cm-body').value.trim();
|
||
if (!name) { document.getElementById('cm-name').focus(); return; }
|
||
if (!email) { document.getElementById('cm-email').focus(); return; }
|
||
const btn = document.getElementById('cm-submit');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Sending…';
|
||
try {
|
||
const resp = await fetch('/api/contact', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ name, email, subject, body })
|
||
});
|
||
if (resp.ok) {
|
||
document.getElementById('cm-form-content').style.display = 'none';
|
||
document.getElementById('cm-success').style.display = 'block';
|
||
} else {
|
||
btn.disabled = false;
|
||
btn.textContent = 'Send message';
|
||
alert('Something went wrong. Please try again.');
|
||
}
|
||
} catch {
|
||
btn.disabled = false;
|
||
btn.textContent = 'Send message';
|
||
alert('Network error. Please try again.');
|
||
}
|
||
}
|
||
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape') closeContact();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|