﻿
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, quote, urlparse
import json
import base64
import html
import mimetypes
import os
import random
import re
import secrets
import smtplib
import shutil
import time
from email.message import EmailMessage

BASE = Path(__file__).resolve().parent
MEDIA = BASE / "website" / "media"
VIDEO_LINKS = MEDIA / "Videography_Links.txt"
LINK_FILES = (VIDEO_LINKS, BASE.parent / "Videography_Links.txt")
SOCIAL_LINKS = MEDIA / "Social_Links.txt"
ESTIMATES = MEDIA / "Estimate_Settings.txt"
EMAIL_CONFIG = BASE.parent / "email_config.txt"
UPLOADS = BASE / "website" / "uploads"
PRIVATE_MEDIA = BASE / "website" / "private"
INQUIRIES = BASE / "website" / "inquiries.jsonl"
PRIVATE_TOKENS = {}
CLIENT_ROOT = BASE / "website" / "clients"
CLIENT_TOKENS = {}
PORT = 8770
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
VIDEO_EXTS = {".mp4", ".webm", ".mov"}
ALL_EXTS = IMAGE_EXTS | VIDEO_EXTS

SLOTS = {
    "showreel": ("Showreel", IMAGE_EXTS | VIDEO_EXTS),
    "hero": ("Hero", IMAGE_EXTS | VIDEO_EXTS),
    "showreelPoster": ("Showreel Poster", ALL_EXTS),
    "featuredFilm": ("Featured Film", IMAGE_EXTS | VIDEO_EXTS),
    "portraitSeries": ("Portrait Series", IMAGE_EXTS | VIDEO_EXTS),
    "eventStory": ("Event Story", IMAGE_EXTS | VIDEO_EXTS),
}
CATEGORIES = ("Portrait", "Events", "Commercial", "Landscape", "Automotive", "Other")

def media_files(folder, extensions=ALL_EXTS):
    folder = MEDIA / folder
    if not folder.is_dir():
        return []
    return sorted(
        (p for p in folder.iterdir() if p.is_file() and p.suffix.lower() in extensions),
        key=lambda p: p.name.casefold(),
    )

def media_url(path):
    relative = path.relative_to(BASE).as_posix()
    return "/" + "/".join(quote(part) for part in relative.split("/"))

def embed_url(value):
    parsed = urlparse(value)
    host = parsed.netloc.lower().split(":", 1)[0]
    path_parts = [part for part in parsed.path.split("/") if part]
    if host in {"youtu.be", "www.youtu.be"} and path_parts:
        return "https://www.youtube.com/embed/" + path_parts[0]
    if "youtube.com" in host:
        video_id = parse_qs(parsed.query).get("v", [None])[0]
        if not video_id and len(path_parts) >= 2 and path_parts[0] in {"shorts", "embed"}:
            video_id = path_parts[1]
        if video_id:
            return "https://www.youtube.com/embed/" + video_id
    if "vimeo.com" in host:
        video_id = next((part for part in reversed(path_parts) if part.isdigit()), None)
        if video_id:
            return "https://player.vimeo.com/video/" + video_id
    if value.startswith("https://www.youtube.com/embed/") or value.startswith("https://player.vimeo.com/video/"):
        return value
    return None

def linked_videos():
    result = []
    seen = set()
    for link_file in LINK_FILES:
        if not link_file.is_file():
            continue
        for raw_line in link_file.read_text(encoding="utf-8-sig").splitlines():
            line = raw_line.strip()
            if not line or line.startswith("#"):
                continue
            match = re.search(r"https?://[^\s|<>\"]+", line)
            if not match:
                continue
            source = match.group(0).rstrip(".,);]")
            if source in seen:
                continue
            seen.add(source)
            embed = embed_url(source)
            if embed:
                parts = [part.strip() for part in line.split("|", 1)]
                result.append({"embed": embed, "url": source, "name": parts[1] if len(parts) > 1 and parts[1] else source, "folder": "Linked video", "kind": "embed"})
    return result

def social_links():
    if not SOCIAL_LINKS.is_file():
        return []
    result = []
    for raw_line in SOCIAL_LINKS.read_text(encoding="utf-8-sig").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#") or "|" not in line:
            continue
        label, url = [part.strip() for part in line.split("|", 1)]
        if label and url.startswith(("https://", "http://")):
            result.append({"label": label, "url": url})
    return result

def estimate_settings():
    services = []
    scopes = []
    if ESTIMATES.is_file():
        for raw_line in ESTIMATES.read_text(encoding="utf-8-sig").splitlines():
            parts = [part.strip() for part in raw_line.split("|")]
            if len(parts) != 3 or raw_line.strip().startswith("#"):
                continue
            try:
                value = float(parts[2])
            except ValueError:
                continue
            item = {"name": parts[1], "value": value}
            if parts[0].upper() == "SERVICE":
                services.append(item)
            elif parts[0].upper() == "SCOPE":
                scopes.append(item)
    return {"services": services, "scopes": scopes}

def email_settings():
    settings = {}
    if not EMAIL_CONFIG.is_file():
        return settings
    for raw_line in EMAIL_CONFIG.read_text(encoding="utf-8-sig").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        settings[key.strip()] = value.strip()
    return settings

def make_quote_pdf(path, lines):
    def esc(value):
        return str(value).replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")[:180]
    content = ["BT", "/F1 16 Tf", "72 748 Td"]
    for index, line in enumerate(lines):
        if index:
            content.append("0 -22 Td")
        content.append("(" + esc(line) + ") Tj")
    content.append("ET")
    stream = "\n".join(content).encode("latin-1", "replace")
    objects = [b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>", b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>", b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"\nendstream"]
    result = bytearray(b"%PDF-1.4\n")
    offsets = [0]
    for number, obj in enumerate(objects, 1):
        offsets.append(len(result))
        result.extend(str(number).encode() + b" 0 obj\n" + obj + b"\nendobj\n")
    xref = len(result)
    result.extend(b"xref\n0 " + str(len(objects) + 1).encode() + b"\n0000000000 65535 f \n")
    for offset in offsets[1:]:
        result.extend((f"{offset:010d} 00000 n \n").encode())
    result.extend(b"trailer\n<< /Size " + str(len(objects) + 1).encode() + b" /Root 1 0 R >>\nstartxref\n" + str(xref).encode() + b"\n%%EOF")
    path.write_bytes(result)

def send_contact_email(fields):
    settings = email_settings()
    username = settings.get("SMTP_USER", "Vimpulse1@gmail.com")
    password = settings.get("SMTP_PASSWORD", "")
    recipient = settings.get("TO_EMAIL", username)
    if not password or password == "PASTE_GOOGLE_APP_PASSWORD_HERE":
        raise RuntimeError("Gmail App Password has not been added to email_config.txt")
    name = str(fields.get("name", "")).strip()
    email = str(fields.get("email", "")).strip()
    subject = str(fields.get("subject", "")).strip()
    inquiry_type = str(fields.get("type", "")).strip()
    message = str(fields.get("message", "")).strip()
    booking_date = str(fields.get("booking_date", "")).strip()
    attachment_name = str(fields.get("attachment_name", "")).strip()
    attachment_data = str(fields.get("attachment_data", "")).strip()
    allowed_subjects = {"Videography", "Photography", "Editing", "IT Support", "Coding", "Software", "General", "Other"}
    allowed_types = {"Quote", "Information", "Job", "Booking", "Collaboration", "Support", "Other"}
    if not name or not email or not subject or not inquiry_type or not message or "\n" in email or "\r" in email:
        raise ValueError("Name, email, subject, type, and message are required")
    if subject not in allowed_subjects or inquiry_type not in allowed_types:
        raise ValueError("Please choose a valid subject and type")
    reference = "VMP-" + secrets.token_hex(4).upper()
    mail = EmailMessage()
    mail["Subject"] = subject + "_" + inquiry_type
    mail["From"] = username
    mail["To"] = recipient
    mail["Reply-To"] = email
    details = "Reference: " + reference + "\nSubject: " + subject + "\nType: " + inquiry_type + "\nName: " + name + "\nEmail: " + email
    if booking_date:
        details += "\nPreferred date: " + booking_date
    mail.set_content(details + "\n\nProject:\n" + message)
    if attachment_name and attachment_data:
        raw_attachment = base64.b64decode(attachment_data, validate=True)
        if len(raw_attachment) > 8 * 1024 * 1024:
            raise ValueError("The attachment must be 8 MB or smaller")
        safe_name = re.sub(r"[^A-Za-z0-9._-]+", "_", Path(attachment_name).name)[:120] or "project-reference"
        UPLOADS.mkdir(parents=True, exist_ok=True)
        (UPLOADS / (secrets.token_hex(8) + "_" + safe_name)).write_bytes(raw_attachment)
        mime_type = mimetypes.guess_type(safe_name)[0] or "application/octet-stream"
        maintype, subtype = mime_type.split("/", 1)
        mail.add_attachment(raw_attachment, maintype=maintype, subtype=subtype, filename=safe_name)
    UPLOADS.mkdir(parents=True, exist_ok=True)
    quote_path = UPLOADS / (reference + "_quote.pdf")
    make_quote_pdf(quote_path, ["VIMPULSE INC. / PROJECT INQUIRY", "", "Reference: " + reference, "Subject: " + subject, "Type: " + inquiry_type, "Name: " + name, "Preferred date: " + (booking_date or "To be confirmed"), "", "Thank you for contacting Vimpulse Inc."])
    INQUIRIES.parent.mkdir(parents=True, exist_ok=True)
    with INQUIRIES.open("a", encoding="utf-8") as inquiry_file:
        inquiry_file.write(json.dumps({"reference": reference, "subject": subject, "type": inquiry_type, "name": name, "email": email, "booking_date": booking_date, "attachment": attachment_name, "quote": quote_path.name}) + "\n")
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=20) as smtp:
        smtp.starttls()
        smtp.login(username, password)
        smtp.send_message(mail)
        confirmation = EmailMessage()
        confirmation["Subject"] = "Vimpulse received your inquiry - " + reference
        confirmation["From"] = username
        confirmation["To"] = email
        confirmation.set_content("Thank you for contacting Vimpulse Inc.\n\nReference: " + reference + "\nWe received your " + subject + " inquiry as a " + inquiry_type + " request. We will review the details and reply as soon as possible.\n\nVimpulse Inc.\nVimpulse@mail.com")
        smtp.send_message(confirmation)
    return reference, "/website/uploads/" + quote_path.name

def payload():
    slots = {}
    for key, (folder, extensions) in SLOTS.items():
        files = media_files(folder, extensions)
        slots[key] = None if not files else {
            "url": media_url(files[0]),
            "name": files[0].name,
            "kind": "video" if files[0].suffix.lower() in VIDEO_EXTS else "image",
        }
    gallery = []
    for category in CATEGORIES:
        for path in media_files(Path("Photography") / category, ALL_EXTS):
            gallery.append({
                "url": media_url(path),
                "name": path.name,
                "category": category,
                "kind": "video" if path.suffix.lower() in VIDEO_EXTS else "image",
            })
    videos = []
    seen_folders = set()
    video_folders = [folder for folder, _extensions in SLOTS.values()]
    video_folders.extend(Path("Photography") / category for category in CATEGORIES)
    for folder in video_folders:
        if folder in seen_folders:
            continue
        seen_folders.add(folder)
        for path in media_files(folder, VIDEO_EXTS):
            videos.append({"url": media_url(path), "name": path.name, "folder": str(folder).replace("\\", " / "), "kind": "video"})
    videos.extend(linked_videos())
    return {"slots": slots, "gallery": gallery, "videos": videos, "socials": social_links(), "categories": CATEGORIES, "estimates": estimate_settings()}

HTML = r'''<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Vimpulse Inc. cinematic videography, photography and creative production."><meta name="robots" content="index, follow, max-image-preview:large"><meta name="theme-color" content="#080808"><link rel="canonical" href="https://vimpulseinc.ca/"><meta property="og:locale" content="en_CA"><meta property="og:site_name" content="Vimpulse Inc."><meta property="og:title" content="Vimpulse Inc. - Visual Stories Built to Last."><meta property="og:description" content="Cinematic videography, photography and creative production by Vimpulse Inc."><meta property="og:type" content="website"><meta property="og:url" content="https://vimpulseinc.ca/"><meta property="og:image" content="https://vimpulseinc.ca/og-image.svg"><meta property="og:image:secure_url" content="https://vimpulseinc.ca/og-image.svg"><meta property="og:image:type" content="image/svg+xml"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="Vimpulse Inc. - Visual Stories Built to Last."><meta name="twitter:description" content="Cinematic videography, photography and creative production by Vimpulse Inc."><meta name="twitter:image" content="https://vimpulseinc.ca/og-image.svg"><script type="application/ld+json">{"@context":"https://schema.org","@type":"ProfessionalService","name":"Vimpulse Inc.","url":"https://vimpulseinc.ca/","description":"Cinematic videography, photography and creative production.","areaServed":"Ontario, Canada","sameAs":["https://vimpulsehub.ca/"],"serviceType":["Videography","Photography","Creative production"]}</script>
<title>Vimpulse Inc. - Visual Stories Built to Last.</title>
<style>
:root{--bg:#080808;--ink:#f4f1eb;--muted:#92908b;--line:#292929;--accent:#d8ff3e}
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font-family:Arial,Helvetica,sans-serif}a{color:inherit;text-decoration:none}button{font:inherit}
.wrap{max-width:1400px;margin:auto;padding:0 5vw}.nav{height:88px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);position:absolute;top:0;left:0;right:0;z-index:5;padding:0 5vw}.brand{font-size:19px;font-weight:800;letter-spacing:.16em}.brand small{display:block;color:var(--muted);font-size:9px;letter-spacing:.24em;margin-top:5px}.navlinks{display:flex;gap:28px;font-size:11px;letter-spacing:.13em;text-transform:uppercase}.navlinks a:hover{color:var(--accent)}
.hero{min-height:100vh;display:grid;align-items:end;position:relative;overflow:hidden;background:radial-gradient(circle at 70% 35%,#28302b 0,#111612 25%,#080808 65%)}.hero:after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(0,0,0,.78),transparent 60%),linear-gradient(0deg,rgba(0,0,0,.7),transparent 45%);pointer-events:none}.hero-media{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.55}.hero-copy{position:relative;z-index:1;padding:0 0 10vh;max-width:900px}.kicker{font-size:11px;letter-spacing:.25em;color:var(--accent);text-transform:uppercase;margin-bottom:25px}.hero h1{font-size:clamp(56px,10vw,150px);line-height:.86;letter-spacing:-.075em;margin:0 0 35px;max-width:900px}.hero p{font-size:14px;letter-spacing:.12em;color:#cbc8c1;text-transform:uppercase}.status{position:absolute;right:5vw;bottom:10vh;z-index:2;color:var(--muted);font:10px monospace;letter-spacing:.15em}.dot{color:var(--accent)}
section{padding:130px 0}.section-head{display:flex;justify-content:space-between;align-items:end;border-bottom:1px solid var(--line);padding-bottom:20px;margin-bottom:45px}.eyebrow{color:var(--muted);font-size:11px;letter-spacing:.2em;text-transform:uppercase}.section-head h2{font-size:clamp(42px,7vw,100px);letter-spacing:-.07em;line-height:.85;margin:0}.work-grid{display:grid;grid-template-columns:1.35fr .8fr;gap:28px}.work-card{position:relative;min-height:420px;background:#131313;border:1px solid var(--line);overflow:hidden}.work-card:nth-child(2){margin-top:120px;min-height:330px}.work-media{width:100%;height:100%;object-fit:cover;display:block;opacity:.78;transition:transform .6s,opacity .6s}.work-card:hover .work-media{transform:scale(1.04);opacity:1}.placeholder{height:100%;min-height:inherit;display:grid;place-items:center;color:#5e5e5e;font-size:12px;letter-spacing:.2em;text-transform:uppercase}.work-label{position:absolute;left:22px;right:22px;bottom:20px;display:flex;justify-content:space-between;font-size:11px;letter-spacing:.12em;text-transform:uppercase}.work-label span:last-child{color:var(--accent)}
.photo-tools{display:flex;gap:10px;flex-wrap:wrap}.filter{border:1px solid var(--line);background:transparent;color:var(--muted);padding:10px 14px;cursor:pointer;font-size:10px;letter-spacing:.15em;text-transform:uppercase}.filter.active,.filter:hover{color:var(--bg);background:var(--accent);border-color:var(--accent)}.gallery{columns:3 220px;column-gap:16px}.photo{break-inside:avoid;margin:0 0 16px;position:relative;cursor:pointer;background:#151515;min-height:180px}.photo img,.photo video{width:100%;display:block;transition:opacity .3s}.photo video{background:#090909}.photo:hover img,.photo:hover video{opacity:.7}.photo figcaption{position:absolute;bottom:12px;left:12px;font-size:10px;letter-spacing:.1em;text-transform:uppercase;text-shadow:0 2px 8px #000}.video-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:28px}.video-card{background:#131313;border:1px solid var(--line);overflow:hidden}.video-card video{width:100%;aspect-ratio:16/9;display:block;object-fit:cover;background:#090909}.video-meta{display:flex;justify-content:space-between;padding:17px 18px;color:var(--muted);font-size:10px;letter-spacing:.13em;text-transform:uppercase}.video-meta span:last-child{color:var(--accent)}.empty{border:1px dashed var(--line);padding:50px;text-align:center;color:var(--muted);font-size:12px;letter-spacing:.1em}
.contact{background:var(--ink);color:#0a0a0a;padding:120px 0}.contact h2{font-size:clamp(52px,10vw,145px);line-height:.82;letter-spacing:-.08em;margin:0 0 45px}.contact a{display:inline-block;border-bottom:1px solid #0a0a0a;padding-bottom:6px;font-size:13px;letter-spacing:.15em;text-transform:uppercase}.footer{padding:24px 0;color:var(--muted);font-size:10px;letter-spacing:.12em;text-transform:uppercase;display:flex;justify-content:space-between}.lightbox{position:fixed;inset:0;background:rgba(0,0,0,.94);z-index:20;display:none;align-items:center;justify-content:center;padding:30px}.lightbox.open{display:flex}.lightbox img{max-width:94vw;max-height:88vh}.close{position:absolute;right:28px;top:22px;color:var(--ink);background:none;border:0;font-size:30px;cursor:pointer}.note{color:var(--muted);font-size:10px;letter-spacing:.12em;text-transform:uppercase;margin-top:15px}
@media(max-width:700px){.nav{height:72px}.navlinks{gap:12px;font-size:9px}.hero-copy{padding-bottom:18vh}.status{bottom:8vh;left:5vw;right:auto}.work-grid{grid-template-columns:1fr}.work-card:nth-child(2){margin-top:0}.section-head{display:block}.photo-tools{margin-top:25px}.footer{display:block;line-height:2}}
</style><style>.video-card iframe{width:100%;aspect-ratio:16/9;display:block;border:0;background:#090909}.about-grid,.service-grid{display:grid;grid-template-columns:1fr 1fr;gap:28px}.about-card,.service-card{border:1px solid var(--line);padding:28px;background:#111}.about-card p,.service-card p{color:var(--muted);line-height:1.7;font-size:14px}.service-card h3{font-size:26px;margin:0 0 12px;letter-spacing:-.04em}.service-card a,.cta{display:inline-block;margin-top:14px;color:var(--bg);background:var(--accent);padding:12px 15px;font-size:10px;letter-spacing:.15em;text-transform:uppercase}.cta-row{display:flex;gap:12px;flex-wrap:wrap}.social-links{display:flex;gap:12px;flex-wrap:wrap}.social-links a{border:1px solid var(--line);padding:12px 15px;color:var(--muted);font-size:10px;letter-spacing:.15em;text-transform:uppercase}.social-links a:hover{color:var(--bg);background:var(--accent)}.contact-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(360px,560px);gap:8vw;align-items:start}.contact-copy{max-width:760px}.contact-copy .eyebrow{color:#6f716a}.contact-copy h2{margin-bottom:35px}.contact-intro{max-width:410px;color:#555950;font-size:15px;line-height:1.65;margin:0 0 28px}.contact-meta{display:flex;gap:22px;flex-wrap:wrap;color:#555950;font-size:10px;letter-spacing:.14em;text-transform:uppercase}.contact-meta span:before{content:'*';color:#789000;margin-right:8px}.contact-form{display:grid;gap:18px;background:rgba(255,255,255,.38);border:1px solid rgba(8,8,8,.18);padding:28px;box-shadow:12px 12px 0 rgba(8,8,8,.08)}.form-kicker{color:#6f716a;font-size:10px;letter-spacing:.18em;text-transform:uppercase;margin:0 0 2px}.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}.field{display:grid;gap:8px}.field label{font-size:10px;letter-spacing:.15em;text-transform:uppercase;color:#555950}.contact-form input,.contact-form textarea{width:100%;background:rgba(244,241,235,.72);border:1px solid rgba(8,8,8,.24);padding:16px;color:#080808;font:inherit;outline:0;transition:border-color .2s,box-shadow .2s}.contact-form input::placeholder,.contact-form textarea::placeholder{color:#777a71}.contact-form input:focus,.contact-form textarea:focus{border-color:#789000;box-shadow:0 0 0 3px rgba(216,255,62,.35)}.contact-form textarea{min-height:155px;resize:vertical}.contact-form .cta{width:100%;border:0;cursor:pointer;text-align:center;padding:16px;margin-top:0;box-shadow:4px 4px 0 #080808;transition:transform .2s,box-shadow .2s}.contact-form .cta:hover{transform:translate(2px,2px);box-shadow:2px 2px 0 #080808}.contact-form .cta:disabled{opacity:.6;cursor:wait}.contact-email{margin:24px 0 0}.contact-email a{font-size:12px}.contact-status{margin:0;color:#555950;font-size:11px;letter-spacing:.08em;min-height:16px}.project-modal{position:fixed;inset:0;background:rgba(0,0,0,.96);z-index:30;display:none;align-items:center;justify-content:center;padding:30px}.project-modal.open{display:flex}.project-panel{width:min(980px,94vw);max-height:90vh;overflow:auto;background:#111;border:1px solid var(--line);padding:28px}.project-panel img,.project-panel video{width:100%;max-height:55vh;object-fit:contain;background:#050505}.project-panel h3{font-size:42px;margin:24px 0 10px}.project-details{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;color:var(--muted);font-size:11px;letter-spacing:.12em;text-transform:uppercase}.project-close{float:right;background:none;border:0;color:var(--ink);font-size:30px;cursor:pointer}@media(max-width:900px){.contact-layout{grid-template-columns:1fr;gap:42px}}@media(max-width:700px){.about-grid,.service-grid{grid-template-columns:1fr}.project-details{grid-template-columns:1fr}.form-row{grid-template-columns:1fr}.contact-form{padding:20px}.contact{padding:90px 0}}</style></head>
<body>
<style>main{display:flex;flex-direction:column}#contact{order:100}</style>
<script>document.addEventListener('contextmenu',function(event){event.preventDefault()});document.addEventListener('dragstart',function(event){if(event.target.matches('img,video'))event.preventDefault()});</script>
<style>.cookie-notice{position:fixed;left:24px;right:24px;bottom:24px;z-index:40;display:flex;align-items:center;justify-content:space-between;gap:20px;padding:14px 16px;background:#f4f1eb;color:#080808;border:1px solid #080808;box-shadow:6px 6px 0 #080808;font-size:11px;letter-spacing:.08em}.cookie-notice .cta{margin:0;white-space:nowrap}@media(max-width:700px){.cookie-notice{left:12px;right:12px;bottom:12px;display:block}.cookie-notice .cta{margin-top:10px}}</style>
<style>.contact-form select{appearance:none;background:rgba(244,241,235,.72);border:1px solid rgba(8,8,8,.24);padding:16px;color:#080808;font:inherit;outline:0;transition:border-color .2s,box-shadow .2s;background-image:linear-gradient(45deg,transparent 50%,#555950 50%),linear-gradient(135deg,#555950 50%,transparent 50%);background-position:calc(100% - 19px) 21px,calc(100% - 14px) 21px;background-size:5px 5px,5px 5px;background-repeat:no-repeat}.contact-form select:focus{border-color:#789000;box-shadow:0 0 0 3px rgba(216,255,62,.35)}</style>
<header class="nav"><a class="brand" href="#top">VIMPULSE<small>INC. / VISUAL DIVISION</small></a><nav class="navlinks"><a href="#about">About</a><a href="#work">Work</a><a href="#services">Services</a><a href="#videography">Video</a><a href="#photography">Photo</a><a href="#contact">Contact</a><a href="https://vimpulsehub.ca" target="_blank" rel="noreferrer">VimpulseHub -&gt;</a></nav></header>
<main id="top"><section class="hero"><div id="heroMedia"></div><div class="wrap hero-copy"><div class="kicker">Film / Photography / Coding</div><h1>Visual stories<br>built to last.</h1><p>Durham Region - Ontario - Canada</p></div><div class="status"><span class="dot">*</span> REC &nbsp; 00:01:42:18</div></section>
<section id="about"><div class="wrap"><div class="section-head"><div><div class="eyebrow">About Vimpulse Inc.</div><h2>Creative meets technical.</h2></div><div class="eyebrow">Durham Region / Ontario / Toronto</div></div><div class="about-grid"><article class="about-card"><div class="eyebrow">The story</div><p>Vimpulse Inc. brings visual storytelling, technology, and practical problem solving together. From filming a meaningful moment to building a useful digital tool, every project is approached with care, precision, and a cinematic eye.</p></article><article class="about-card"><div class="eyebrow">Service area</div><p>Based in Durham Region and serving Whitby, Oshawa, Ajax, Pickering, Toronto, and nearby Ontario communities. Available for local productions, remote creative work, and technical projects.</p></article></div></div></section>
<section id="work"><div class="wrap"><div class="section-head"><div><div class="eyebrow">Selected work / 01-06</div><h2>Made to move.</h2></div><div class="eyebrow">VIMPULSE (R) / 2026</div></div><div class="work-grid" id="workGrid"></div></div></section>
<section id="services"><div class="wrap"><div class="section-head"><div><div class="eyebrow">What we do</div><h2>Built for the brief.</h2></div><div class="eyebrow">Creative / Technical</div></div><div class="service-grid"><article class="service-card"><h3>Videography</h3><p>Short films, events, brand stories, YouTube content, and cinematic production designed to hold attention.</p><a href="#contact">Request a quote</a></article><article class="service-card"><h3>Photography</h3><p>Portraits, events, lifestyle, automotive, commercial, and documentary photography with a strong editorial finish.</p><a href="#contact">Request a quote</a></article><article class="service-card"><h3>Editing</h3><p>Video editing, colour, sound, delivery, and social-ready versions that turn footage into a finished story.</p><a href="#contact">Request a quote</a></article><article class="service-card"><h3>IT Support</h3><p>Computer troubleshooting, systems support, setup, optimization, and dependable technical guidance.</p><a href="#contact">Request a quote</a></article><article class="service-card"><h3>Coding</h3><p>Practical websites, tools, automation, and custom software built around real workflow needs.</p><a href="#contact">Request a quote</a></article><article class="service-card"><h3>Software</h3><p>Useful Windows programs and digital products developed with clarity, maintainability, and the user in mind.</p><a href="#contact">Request a quote</a></article></div></div></section>
<section id="videography"><div class="wrap"><div class="section-head"><div><div class="eyebrow">Videography / motion archive</div><h2>Motion in focus.</h2></div><div class="eyebrow">FILM / VIDEO</div></div><div class="video-grid" id="videoGrid"></div></div></section>
<section id="photography"><div class="wrap"><div class="section-head"><div><div class="eyebrow">Photography / archive</div><h2>Frames of life.</h2></div><div class="photo-tools" id="filters"></div></div><div class="gallery" id="gallery"></div></div></section>
<section id="socials"><div class="wrap"><div class="section-head"><div><div class="eyebrow">Find Vimpulse online</div><h2>Keep in touch.</h2></div></div><div class="social-links" id="socialLinks"></div></div></section>
<section id="contact" class="contact"><div class="wrap contact-layout"><div class="contact-copy"><div class="eyebrow">Start a project</div><h2>Let's make<br>something real.</h2><p class="contact-intro">Have a story, idea, event, or technical challenge in mind? Send a few details and Vimpulse Inc. will get back to you directly.</p><div class="contact-meta"><span>Durham Region</span><span>Toronto / Ontario</span><span>Creative + Technical</span></div><p class="contact-email"><a href="mailto:Vimpulse@mail.com">Vimpulse@mail.com -&gt;</a></p></div><form class="contact-form" id="contact-form"><p class="form-kicker">Project inquiry / 01</p><div class="form-row"><div class="field"><label for="contact-subject">Subject</label><select id="contact-subject" name="subject" required><option value="" selected disabled>Choose a service</option><option>Videography</option><option>Photography</option><option>Editing</option><option>IT Support</option><option>Coding</option><option>Software</option><option>General</option><option>Other</option></select></div><div class="field"><label for="contact-type">Type</label><select id="contact-type" name="type" required><option value="" selected disabled>Choose inquiry type</option><option>Quote</option><option>Information</option><option>Job</option><option>Booking</option><option>Collaboration</option><option>Support</option><option>Other</option></select></div></div><div class="form-row"><div class="field"><label for="contact-name">Your name</label><input id="contact-name" name="name" placeholder="Michael Smith" autocomplete="name" required></div><div class="field"><label for="contact-email">Email address</label><input id="contact-email" name="email" type="email" placeholder="you@example.com" autocomplete="email" required></div></div><div class="field"><label for="contact-message">Tell us about the project</label><textarea id="contact-message" name="message" placeholder="What are you looking to create? Include timing, location, and the kind of support you need." required></textarea></div><button class="cta" type="submit">Send project request -&gt;</button><p class="contact-status" id="contactStatus" aria-live="polite"></p></form></div></section></main>
<footer class="wrap footer"><span>(C) 2026 Vimpulse Inc.</span><span><a href="/privacy.html">Privacy</a> / Creative production / technology</span></footer><div class="lightbox" id="lightbox"><button class="close" aria-label="Close">x</button><img alt="Expanded photograph"></div><div class="project-modal" id="projectModal"><div class="project-panel"><button class="project-close" aria-label="Close">x</button><div id="projectMedia"></div><h3 id="projectTitle"></h3><p id="projectDescription"></p><div class="project-details"><span id="projectCredits"></span><span id="projectLocation"></span><span id="projectDate"></span></div></div></div>
<script>
const slotLabels={featuredFilm:['Featured Film','FILM / PROJECT'],portraitSeries:['Portrait Series','PHOTOGRAPHY / PORTRAIT'],eventStory:['Event Story','FILM / EVENTS']};let data;
function mediaElement(item,poster){if(!item)return '<div class="placeholder">Drop media into its folder</div>';if(item.kind==='video')return `<video class="work-media" src="${item.url}" ${poster?'poster="'+poster+'"':''} autoplay muted loop playsinline></video>`;return `<img class="work-media" src="${item.url}" alt="${item.name}">`}
function renderHero(){const item=data.slots.hero||data.slots.showreel;const host=document.getElementById('heroMedia');if(!item)return;host.innerHTML=item.kind==='video'?`<video class="hero-media" src="${item.url}" autoplay muted loop playsinline></video>`:`<img class="hero-media" src="${item.url}" alt="Vimpulse cinematic hero">`}
function renderWork(){const s=data.slots;const poster=s.showreelPoster?.kind==='image'?s.showreelPoster.url:null;const cards=Object.entries(slotLabels).filter(([key])=>s[key]).map(([key,label])=>`<article class="work-card">${mediaElement(s[key],poster)}<div class="work-label"><span>${label[0]}<br><small>${label[1]}</small></span><span>-&gt;</span></div></article>`).join('');document.getElementById('work').style.display=cards?'':'none';document.getElementById('workGrid').innerHTML=cards}
function renderVideos(){const section=document.getElementById('videography');if(!data.videos.length){section.style.display='none';return}section.style.display='';document.getElementById('videoGrid').innerHTML=data.videos.map(x=>`<article class="video-card"><video src="${x.url}" controls muted playsinline preload="metadata"></video><div class="video-meta"><span>${x.folder}<br>${x.name}</span><span>VIDEO -&gt;</span></div></article>`).join('')}
function renderFilters(){const f=document.getElementById('filters');['All',...data.categories].forEach((cat,i)=>{const b=document.createElement('button');b.className='filter'+(i===0?' active':'');b.textContent=cat;b.onclick=()=>{document.querySelectorAll('.filter').forEach(x=>x.classList.remove('active'));b.classList.add('active');renderGallery(cat)};f.appendChild(b)})}
function openProject(item){const modal=document.getElementById('projectModal');const media=document.getElementById('projectMedia');media.innerHTML='';let element;if(item.kind==='embed'){element=document.createElement('iframe');element.src=item.embed;element.allow='autoplay; fullscreen; picture-in-picture';element.allowFullscreen=true;element.style.width='100%';element.style.aspectRatio='16/9';element.style.border='0'}else if(item.kind==='video'){element=document.createElement('video');element.src=item.url;element.controls=true;element.playsInline=true}else{element=document.createElement('img');element.src=item.url;element.alt=item.name||'Vimpulse project'}media.appendChild(element);document.getElementById('projectTitle').textContent=item.name||'Vimpulse Project';document.getElementById('projectDescription').textContent='A Vimpulse Inc. creative project combining visual storytelling, careful production, and a finished cinematic presentation.';document.getElementById('projectCredits').textContent='Credits: Vimpulse Inc.';document.getElementById('projectLocation').textContent='Location: Durham Region / Ontario / Toronto';document.getElementById('projectDate').textContent='Date: 2026';modal.classList.add('open')}
function wireProjects(){const keys=Object.entries(slotLabels).filter(([key])=>data.slots[key]).map(([key])=>key);document.querySelectorAll('#workGrid .work-card').forEach((card,index)=>{card.onclick=()=>openProject(data.slots[keys[index]])})}
function renderSocials(){const section=document.getElementById('socials');const links=document.getElementById('socialLinks');if(!data.socials.length){section.style.display='none';return}section.style.display='';links.innerHTML='';data.socials.forEach(x=>{const a=document.createElement('a');a.href=x.url;a.target='_blank';a.rel='noreferrer';a.textContent=x.label;links.appendChild(a)})}
function wireGallery(){document.querySelectorAll('#gallery .photo').forEach(card=>{card.onclick=()=>{const image=card.querySelector('img');const video=card.querySelector('video');openProject({url:image?image.src:video.src,name:card.textContent.trim(),kind:image?'image':'video'})}})}
function setupContact(){const form=document.getElementById('contact-form');const status=document.getElementById('contactStatus');form.onsubmit=async e=>{e.preventDefault();status.textContent='Sending...';const values=Object.fromEntries(new FormData(form).entries());try{const response=await fetch('/api/contact',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(values)});const result=await response.json();if(!response.ok||!result.ok)throw new Error(result.error||'Email could not be sent');status.textContent='Message sent. Thank you.';form.reset()}catch(error){status.textContent=error.message+' Please check the server email settings.'}}}
function renderVideos(){const section=document.getElementById('videography');const grid=document.getElementById('videoGrid');if(!data.videos.length){section.style.display='none';return}section.style.display='';grid.innerHTML='';data.videos.forEach(x=>{const card=document.createElement('article');card.className='video-card';if(x.kind==='embed'){const frame=document.createElement('iframe');frame.src=x.embed;frame.title=x.name;frame.loading='lazy';frame.allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';frame.allowFullscreen=true;card.appendChild(frame)}else{const video=document.createElement('video');video.src=x.url;video.controls=true;video.muted=true;video.playsInline=true;video.preload='metadata';card.appendChild(video)}const meta=document.createElement('div');meta.className='video-meta';const label=document.createElement('span');label.textContent=x.folder+' / '+x.name;const type=document.createElement('span');type.textContent=x.kind==='embed'?'EMBED ->':'VIDEO ->';meta.append(label,type);card.appendChild(meta);grid.appendChild(card)})}
function renderGallery(category='All'){const section=document.getElementById('photography');if(!data.gallery.length){section.style.display='none';return}section.style.display='';const items=data.gallery.filter(x=>category==='All'||x.category===category);const g=document.getElementById('gallery');g.innerHTML=items.map(x=>`<figure class="photo" data-url="${x.url}">${x.kind==='video'?`<video src="${x.url}" controls muted playsinline preload="metadata"></video>`:`<img src="${x.url}" alt="${x.category} - ${x.name}" loading="lazy">`}<figcaption>${x.category} / ${x.name}</figcaption></figure>`).join('');g.querySelectorAll('.photo').forEach(p=>p.onclick=()=>{const image=p.querySelector('img');if(image){document.querySelector('#lightbox img').src=image.src;document.getElementById('lightbox').classList.add('open')}})}
fetch('/api/media').then(r=>r.json()).then(x=>{data=x;renderHero();renderWork();renderVideos();renderFilters();renderGallery();renderSocials();wireProjects();wireGallery();setupContact()}).catch(()=>{data={slots:{},gallery:[],videos:[],socials:[],categories:[]};renderWork();renderVideos();renderGallery();renderSocials()});document.querySelector('.close').onclick=()=>document.getElementById('lightbox').classList.remove('open');document.querySelector('.project-close').onclick=()=>document.getElementById('projectModal').classList.remove('open');document.getElementById('projectModal').onclick=e=>{if(e.target.id==='projectModal')e.currentTarget.classList.remove('open')};document.getElementById('lightbox').onclick=e=>{if(e.target.id==='lightbox')e.currentTarget.classList.remove('open')};
</script></body></html>'''

EXTRA_HTML = r'''<section id="packages"><div class="wrap"><div class="section-head"><div><div class="eyebrow">Starting points</div><h2>Choose your brief.</h2></div><div class="eyebrow">Flexible / Custom</div></div><div class="service-grid"><article class="service-card"><div class="eyebrow">01 / Motion</div><h3>Film + video</h3><p>Production, event coverage, brand stories, and finished edits built around your goal.</p><a href="#contact">Discuss a project</a></article><article class="service-card"><div class="eyebrow">02 / Still</div><h3>Photo story</h3><p>Portraits, events, commercial work, and editorial galleries with a consistent visual finish.</p><a href="#contact">Discuss a project</a></article><article class="service-card"><div class="eyebrow">03 / Digital</div><h3>Creative systems</h3><p>Websites, tools, automation, and software that make the work easier to manage.</p><a href="#contact">Discuss a project</a></article></div><p class="note">Every project is quoted after the brief, location, timeline, and deliverables are understood.</p><p><a class="cta" href="/service-guide.txt" download>Download service guide -&gt;</a></p></div></section><section id="testimonials"><div class="wrap"><div class="section-head"><div><div class="eyebrow">Client perspective</div><h2>Good work travels.</h2></div></div><div class="about-grid"><article class="about-card"><div class="eyebrow">Your words here</div><p>Client testimonials can be added to this section as projects are completed.</p><p class="note">Replace this text with a real client quote and name.</p></article><article class="about-card"><div class="eyebrow">A better brief</div><p>Clear communication, thoughtful production, and dependable delivery from the first conversation to the final file.</p></article></div></div></section><section id="faq"><div class="wrap"><div class="section-head"><div><div class="eyebrow">Questions</div><h2>Before we begin.</h2></div></div><div class="about-grid"><details class="about-card"><summary>What areas do you serve?</summary><p>Vimpulse Inc. serves Durham Region, Whitby, Oshawa, Ajax, Pickering, Toronto, and nearby Ontario communities, with remote creative and technical work also available.</p></details><details class="about-card"><summary>Can I send reference files?</summary><p>Yes. Use the attachment field in the contact form for one reference image, PDF, or small project file up to 8 MB.</p></details><details class="about-card"><summary>How quickly will I hear back?</summary><p>Every inquiry is reviewed directly. Include your timing and preferred date so the reply can be more useful.</p></details><details class="about-card"><summary>Do you offer consultations?</summary><p>Yes. Choose Booking in the inquiry type and add your preferred date. A consultation time can then be arranged by email.</p></details></div></div></section><div class="cookie-notice" id="cookieNotice"><span>This site uses only essential browser storage for your privacy choice.</span><button class="cta" id="cookieAccept" type="button">Continue -&gt;</button></div><script>(function(){const form=document.getElementById('contact-form');if(!form)return;const row=document.createElement('div');row.className='form-row';row.innerHTML='<div class="field"><label for="contact-date">Preferred date</label><input id="contact-date" name="booking_date" type="date"></div><div class="field"><label for="contact-file">Reference file <small>(optional, 8 MB max)</small></label><input id="contact-file" name="attachment" type="file" accept=".jpg,.jpeg,.png,.webp,.gif,.pdf,.mp4,.webm,.mov"></div>';const message=form.querySelector('#contact-message');message.parentNode.before(row);const trap=document.createElement('input');trap.name='website';trap.tabIndex=-1;trap.autocomplete='off';trap.style.cssText='position:absolute;left:-9999px;opacity:0';form.appendChild(trap);form.addEventListener('submit',async function(event){event.preventDefault();event.stopImmediatePropagation();const status=document.getElementById('contactStatus');const button=form.querySelector('button[type=submit]');const values=Object.fromEntries(new FormData(form).entries());if(values.website){return}delete values.website;const file=document.getElementById('contact-file').files[0];if(file){if(file.size>8*1024*1024){status.textContent='Please choose a file smaller than 8 MB.';return}values.attachment_name=file.name;values.attachment_data=await new Promise((resolve,reject)=>{const reader=new FileReader();reader.onload=()=>resolve(String(reader.result).split(',')[1]||'');reader.onerror=reject;reader.readAsDataURL(file)});}delete values.attachment;button.disabled=true;status.textContent='Sending...';try{const response=await fetch('/api/contact',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(values)});const result=await response.json();if(!response.ok||!result.ok)throw new Error(result.error||'Email could not be sent');status.textContent='Message sent. A confirmation email is on its way.';form.reset()}catch(error){status.textContent=error.message+' Please check the server email settings.'}finally{button.disabled=false}},true);const notice=document.getElementById('cookieNotice');if(localStorage.getItem('vimpulse_cookie_ok'))notice.remove();document.getElementById('cookieAccept').onclick=()=>{localStorage.setItem('vimpulse_cookie_ok','1');notice.remove()};})();</script>'''

PRIVACY_HTML = '''<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Privacy - Vimpulse Inc.</title><style>body{margin:0;background:#080808;color:#f4f1eb;font-family:Arial,Helvetica,sans-serif;line-height:1.7}.wrap{max-width:820px;margin:auto;padding:80px 6vw}a{color:#d8ff3e}h1{font-size:clamp(48px,8vw,96px);line-height:.9;letter-spacing:-.07em}h2{margin-top:45px}</style></head><body><main class="wrap"><a href="/">VIMPULSE INC. / HOME</a><h1>Privacy</h1><p>Vimpulse Inc. respects your privacy. This website may receive information you choose to send when you contact us, such as your name, email address, project details, and optional reference file.</p><h2>How information is used</h2><p>Information is used only to respond to inquiries, discuss projects, provide requested services, and improve the website. We do not sell personal information. Contact form attachments are stored with the website for project follow-up.</p><h2>Media and external services</h2><p>This site may display locally hosted media and embedded videos from services such as YouTube or Vimeo. Those services may process data according to their own privacy policies.</p><h2>Contact</h2><p>Questions about privacy can be sent to <a href="mailto:Vimpulse@mail.com">Vimpulse@mail.com</a>.</p><p>Last updated: 2026</p></main></body></html>'''
SERVICE_GUIDE = '''VIMPULSE INC. / SERVICE GUIDE\n\nVideography - cinematic films, events, brand stories, and video content.\nPhotography - portraits, events, commercial, automotive, and editorial work.\nEditing - video editing, colour, sound, delivery, and social versions.\nIT Support - troubleshooting, setup, optimization, and technical guidance.\nCoding - websites, tools, automation, and custom workflow solutions.\nSoftware - Windows programs and digital products.\n\nService area: Durham Region, Whitby, Oshawa, Ajax, Pickering, Toronto, and nearby Ontario communities.\n\nRequest a quote: https://vimpulseinc.ca/#contact\n'''
SITEMAP = '''<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://vimpulseinc.ca/</loc></url><url><loc>https://vimpulseinc.ca/privacy.html</loc></url></urlset>'''
ROBOTS = '''User-agent: *\nAllow: /\nSitemap: https://vimpulseinc.ca/sitemap.xml\n'''
NOT_FOUND_HTML = '''<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Page not found - Vimpulse Inc.</title><style>body{margin:0;background:#080808;color:#f4f1eb;font-family:Arial,sans-serif}.wrap{max-width:900px;margin:auto;padding:18vh 6vw}a{color:#d8ff3e;text-transform:uppercase;letter-spacing:.16em;font-size:12px}h1{font-size:clamp(64px,12vw,160px);line-height:.8;letter-spacing:-.08em;margin:35px 0}</style></head><body><main class="wrap"><a href="/">VIMPULSE INC. / HOME</a><h1>Page not<br>found.</h1><p>The frame you are looking for is not here.</p></main></body></html>'''
OG_SVG = '''<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630"><rect width="1200" height="630" fill="#080808"/><circle cx="920" cy="140" r="360" fill="#19231d"/><circle cx="1030" cy="520" r="270" fill="#243b2d"/><path d="M0 520 L1200 420 L1200 630 L0 630Z" fill="#d8ff3e"/><text x="76" y="92" fill="#d8ff3e" font-family="Arial,Helvetica,sans-serif" font-size="18" letter-spacing="6">VIMPULSE INC. / VISUAL DIVISION</text><text x="76" y="300" fill="#f4f1eb" font-family="Arial,Helvetica,sans-serif" font-size="86" font-weight="700">VISUAL STORIES</text><text x="76" y="395" fill="#f4f1eb" font-family="Arial,Helvetica,sans-serif" font-size="86" font-weight="700">BUILT TO LAST.</text><text x="78" y="558" fill="#080808" font-family="Arial,Helvetica,sans-serif" font-size="22" letter-spacing="4">FILM / PHOTOGRAPHY / PRODUCTION</text></svg>'''
LAST_SHARE_PHOTO = None

def private_files():
    PRIVATE_MEDIA.mkdir(parents=True, exist_ok=True)
    return [path for path in sorted(PRIVATE_MEDIA.iterdir(), key=lambda item: item.name.casefold()) if path.is_file() and path.suffix.lower() in IMAGE_EXTS | VIDEO_EXTS]

def client_media(folder_name):
    folder = CLIENT_ROOT / Path(folder_name).name
    if folder.parent != CLIENT_ROOT or not folder.is_dir():
        return []
    supported = {".jpg", ".jpeg", ".png", ".mp4"}
    return [path for path in sorted(folder.iterdir(), key=lambda item: item.name.casefold()) if path.is_file() and path.suffix.lower() in supported]

def admin_page():
    rows = []
    if INQUIRIES.is_file():
        for line in INQUIRIES.read_text(encoding="utf-8").splitlines():
            try:
                item = json.loads(line)
            except json.JSONDecodeError:
                continue
            rows.append("<tr><td>" + html.escape(item.get("reference", "")) + "</td><td>" + html.escape(item.get("subject", "")) + " / " + html.escape(item.get("type", "")) + "</td><td>" + html.escape(item.get("name", "")) + "<br>" + html.escape(item.get("email", "")) + "</td><td>" + html.escape(item.get("attachment", "None")) + "</td></tr>")
    table = "".join(rows) or "<tr><td colspan='4'>No inquiries yet.</td></tr>"
    return "<!doctype html><html><head><meta charset='utf-8'><title>Vimpulse Admin</title><style>body{background:#080808;color:#f4f1eb;font:14px Arial;padding:40px}table{width:100%;border-collapse:collapse}td,th{border:1px solid #333;padding:12px;text-align:left}th{color:#d8ff3e}</style></head><body><h1>Vimpulse inquiries</h1><table><tr><th>Reference</th><th>Subject / Type</th><th>Client</th><th>Attachment</th></tr>" + table + "</table></body></html>"

def share_html():
    global LAST_SHARE_PHOTO
    photos = []
    for category in CATEGORIES:
        photos.extend(media_files(Path("Photography") / category, IMAGE_EXTS))
    image_url = "https://vimpulseinc.ca/og-image.svg"
    if photos:
        available = [photo for photo in photos if photo != LAST_SHARE_PHOTO] or photos
        selected = random.choice(available)
        LAST_SHARE_PHOTO = selected
        image_url = "https://vimpulseinc.ca" + media_url(selected) + "?share=" + secrets.token_hex(6)
    quiz_html = r'''<section id="project-builder"><div class="wrap"><div class="section-head"><div><div class="eyebrow">Interactive brief builder</div><h2>Build your project.</h2></div><div class="eyebrow">01 / 04</div></div><div class="builder-card"><div class="builder-progress"><span id="builderProgress"></span></div><div class="builder-step active" data-step="1"><div class="eyebrow">What are you looking for?</div><div class="builder-options"><button type="button" data-value="Videography">Film / Video</button><button type="button" data-value="Photography">Photography</button><button type="button" data-value="Editing">Editing</button><button type="button" data-value="IT Support">IT Support</button><button type="button" data-value="Coding">Coding</button><button type="button" data-value="Software">Software</button></div></div><div class="builder-step" data-step="2"><div class="eyebrow">What should it feel like?</div><div class="builder-options"><button type="button" data-value="Cinematic">Cinematic</button><button type="button" data-value="Documentary">Documentary</button><button type="button" data-value="Clean and modern">Clean / Modern</button><button type="button" data-value="Energetic">Energetic</button><button type="button" data-value="Dark and dramatic">Dark / Dramatic</button></div></div><div class="builder-step" data-step="3"><div class="eyebrow">What is the scope?</div><div class="builder-options"><button type="button" data-value="One day">One day</button><button type="button" data-value="Several days">Several days</button><button type="button" data-value="Ongoing support">Ongoing support</button><button type="button" data-value="Not sure yet">Not sure yet</button></div></div><div class="builder-step" data-step="4"><div class="eyebrow">When are you hoping to start?</div><div class="builder-options"><button type="button" data-value="As soon as possible">As soon as possible</button><button type="button" data-value="Within a month">Within a month</button><button type="button" data-value="In the next few months">Next few months</button><button type="button" data-value="Just exploring">Just exploring</button></div></div><div class="builder-result" id="builderResult"><div class="eyebrow">Your project direction</div><p id="builderSummary"></p><button class="cta" id="builderContinue" type="button">Continue to contact form -&gt;</button></div></div></div></section><style>.builder-card{max-width:900px;border:1px solid var(--line);background:#111;padding:30px}.builder-progress{height:3px;background:#292929;margin-bottom:42px}.builder-progress span{display:block;width:25%;height:100%;background:var(--accent);transition:width .3s}.builder-step{display:none}.builder-step.active{display:block}.builder-options{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:24px}.builder-options button{border:1px solid var(--line);background:#151515;color:var(--ink);padding:18px;text-align:left;cursor:pointer;font-size:12px;letter-spacing:.12em;text-transform:uppercase;transition:background .2s,color .2s,border-color .2s}.builder-options button:hover{border-color:var(--accent);background:var(--accent);color:var(--bg)}.builder-result{display:none}.builder-result p{font-size:24px;line-height:1.3;max-width:720px}.builder-result .cta{border:0;cursor:pointer;margin-top:12px}@media(max-width:700px){.builder-card{padding:20px}.builder-options{grid-template-columns:1fr}}</style><script>(function(){const root=document.getElementById('project-builder');if(!root)return;const answers=[];let step=1;const progress=document.getElementById('builderProgress');const steps=[...root.querySelectorAll('.builder-step')];const update=()=>{steps.forEach(x=>x.classList.toggle('active',Number(x.dataset.step)===step));progress.style.width=(step*25)+'%'};root.querySelectorAll('.builder-options button').forEach(button=>button.addEventListener('click',()=>{answers[step-1]=button.dataset.value;if(step<4){step++;update()}else{steps.forEach(x=>x.classList.remove('active'));progress.style.width='100%';document.getElementById('builderResult').style.display='block';document.getElementById('builderSummary').textContent=answers[0]+' / '+answers[1]+' / '+answers[2]+' / '+answers[3]}}));document.getElementById('builderContinue').addEventListener('click',()=>{const subject=document.getElementById('contact-subject');if(subject)subject.value=answers[0]==='Film / Video'?'Videography':answers[0];const message=document.getElementById('contact-message');if(message)message.value='Project direction: '+answers[0]+'\nVisual direction: '+answers[1]+'\nScope: '+answers[2]+'\nTimeline: '+answers[3]+'\n\nAdditional details: ';document.getElementById('contact').scrollIntoView({behavior:'smooth'})});})();</script>'''
    features_html = r'''<section id="interactive-tools"><div class="wrap"><div class="section-head"><div><div class="eyebrow">Plan the next move</div><h2>Make it real.</h2></div><div class="eyebrow">Tools / Client-ready</div></div><div class="about-grid"><article class="about-card"><div class="eyebrow">Instant estimate</div><h3>Find a starting range.</h3><p>Choose a service and scope to see a planning estimate before the conversation.</p><select id="estimateService"><option value="0">Choose a service</option><option value="650">Photography</option><option value="1200">Videography</option><option value="450">Editing</option><option value="300">IT Support</option><option value="900">Coding</option><option value="800">Software</option></select><select id="estimateScope"><option value="0">Choose the scope</option><option value="1">Short / focused</option><option value="2">Half day / standard</option><option value="4">Full day / expanded</option><option value="6">Ongoing / custom</option></select><button class="cta" id="estimateButton" type="button">Calculate range -&gt;</button><p id="estimateResult" class="note"></p></article><article class="about-card"><div class="eyebrow">Explore a project</div><h3>Let the archive choose.</h3><p>Take a quick spin through the work and discover a random project from the current media folders.</p><button class="cta" id="exploreProject" type="button">Explore a project -&gt;</button></article></div></div></section><section id="timeline"><div class="wrap"><div class="section-head"><div><div class="eyebrow">How it works</div><h2>From brief to delivery.</h2></div></div><div class="about-grid"><article class="about-card"><div class="eyebrow">01 / Brief</div><h3>We listen.</h3><p>Goals, audience, timing, location, and the feeling you want people to remember.</p></article><article class="about-card"><div class="eyebrow">02 / Plan</div><h3>We shape it.</h3><p>A clear direction, production plan, shot list, or technical approach.</p></article><article class="about-card"><div class="eyebrow">03 / Create</div><h3>We make it.</h3><p>Production, photography, coding, editing, and focused communication throughout.</p></article><article class="about-card"><div class="eyebrow">04 / Deliver</div><h3>You launch it.</h3><p>Finished files, useful tools, and a clean handoff ready for the real world.</p></article></div><div class="progress-track"><span style="width:25%"></span></div></div></section><section id="service-area"><div class="wrap"><div class="section-head"><div><div class="eyebrow">Local production</div><h2>Close to the story.</h2></div><div class="eyebrow">Durham Region / Toronto</div></div><div class="map-card"><div><h3>Serving Ontario communities.</h3><p>Durham Region, Whitby, Oshawa, Ajax, Pickering, Toronto, and nearby areas.</p><a class="cta" href="https://www.google.com/maps/search/Durham+Region+Ontario" target="_blank" rel="noreferrer">Open service area map -&gt;</a></div><iframe title="Durham Region and Toronto service area" loading="lazy" src="https://www.openstreetmap.org/export/embed.html?bbox=-79.7%2C43.65%2C-78.6%2C44.2&amp;layer=mapnik"></iframe></div></div></section><section id="client-upload"><div class="wrap"><div class="section-head"><div><div class="eyebrow">Client upload portal</div><h2>Send larger files.</h2></div></div><div class="about-card"><p>Already have an inquiry reference number? Upload one larger project file here, up to 50 MB.</p><form id="upload-form"><div class="form-row"><input name="reference" placeholder="Inquiry reference e.g. VMP-1234ABCD" required><input name="file" type="file" required></div><button class="cta" type="submit">Upload project file -&gt;</button><p id="uploadStatus" class="note" aria-live="polite"></p></form></div></div></section><style>.about-card select,.map-card{width:100%;background:#151515;color:var(--ink);border:1px solid var(--line);padding:14px;margin:6px 0}.about-card select{font:inherit}.progress-track{height:8px;background:#292929;margin-top:36px}.progress-track span{display:block;height:100%;background:var(--accent);animation:progressPulse 2s ease-in-out infinite alternate}@keyframes progressPulse{from{width:25%}to{width:90%}}.map-card{display:grid;grid-template-columns:1fr 1fr;gap:24px}.map-card h3{font-size:32px;margin:0 0 12px}.map-card p{color:var(--muted);line-height:1.7}.map-card iframe{width:100%;min-height:280px;border:0;filter:grayscale(1) contrast(1.1)}.photo{overflow:hidden}.photo:after{content:'VIMPULSE INC.';position:absolute;right:10px;top:10px;color:rgba(255,255,255,.68);font-size:9px;letter-spacing:.18em;transform:rotate(-10deg);pointer-events:none}.about-card input{width:100%;padding:14px;background:#f4f1eb;border:1px solid var(--line);font:inherit}@media(max-width:700px){.map-card{grid-template-columns:1fr}}</style><script>(function(){const estButton=document.getElementById('estimateButton');if(estButton){estButton.onclick=()=>{const base=Number(document.getElementById('estimateService').value),mult=Number(document.getElementById('estimateScope').value);const out=document.getElementById('estimateResult');if(!base||!mult){out.textContent='Choose a service and scope first.';return}const low=base*mult,high=Math.round(low*1.65);out.textContent='Planning range: $'+low.toLocaleString()+' - $'+high.toLocaleString()+' CAD';}}const explore=document.getElementById('exploreProject');if(explore)explore.onclick=()=>{if(!window.data||(!data.gallery.length&&!data.videos.length)){document.getElementById('contact').scrollIntoView({behavior:'smooth'});return}const items=[...(data.gallery||[]),...(data.videos||[])];const item=items[Math.floor(Math.random()*items.length)];if(item.kind==='image'){document.querySelector('#lightbox img').src=item.url;document.getElementById('lightbox').classList.add('open')}else{openProject(item)}};const timer=setInterval(()=>{document.querySelectorAll('.video-card video').forEach(video=>{video.autoplay=true;video.loop=true;video.muted=true;video.playsInline=true;video.play().catch(()=>{})})},1200);const upload=document.getElementById('upload-form');if(upload)upload.onsubmit=async e=>{e.preventDefault();const status=document.getElementById('uploadStatus');const file=upload.querySelector('[name=file]').files[0];if(!file)return;if(file.size>50*1024*1024){status.textContent='Please choose a file smaller than 50 MB.';return}status.textContent='Uploading...';const reader=new FileReader();reader.onload=async()=>{try{const response=await fetch('/api/client-upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({reference:upload.querySelector('[name=reference]').value,name:file.name,data:String(reader.result).split(',')[1]})});const result=await response.json();if(!response.ok||!result.ok)throw new Error(result.error||'Upload failed');status.textContent='Upload received: '+result.name;upload.reset()}catch(error){status.textContent=error.message}};reader.readAsDataURL(file)};})();</script>'''
    features_html += r'''<script>(function(){const result=document.getElementById('builderResult');if(!result)return;const button=document.createElement('button');button.className='cta';button.type='button';button.textContent='Save my project brief ->';button.onclick=()=>{const text='VIMPULSE INC. / PROJECT BRIEF\n\n'+document.getElementById('builderSummary').textContent+'\n\nAdd your details at https://vimpulseinc.ca/#contact';const blob=new Blob([text],{type:'text/plain'});const link=document.createElement('a');link.href=URL.createObjectURL(blob);link.download='vimpulse-project-brief.txt';link.click();setTimeout(()=>URL.revokeObjectURL(link.href),500)};result.appendChild(button)})();</script>'''
    features_html += r'''<script>(function(){const service=document.getElementById('estimateService');const scope=document.getElementById('estimateScope');if(!service||!scope)return;const timer=setInterval(()=>{if(typeof data==='undefined'||!data.estimates)return;const fill=(select,items,placeholder)=>{if(select.dataset.loaded)return;select.innerHTML='<option value="0">'+placeholder+'</option>';items.forEach(item=>{const option=document.createElement('option');option.value=item.value;option.textContent=item.name;select.appendChild(option)});select.dataset.loaded='1'};fill(service,data.estimates.services,'Choose a service');fill(scope,data.estimates.scopes,'Choose the scope');if(service.dataset.loaded&&scope.dataset.loaded)clearInterval(timer)},200)})();</script>'''
    features_html = features_html.replace("if(!window.data||(!data.gallery.length&&!data.videos.length))", "if(typeof data==='undefined'||(!data.gallery.length&&!data.videos.length))")
    extra = re.sub(r'<section id="testimonials">.*?</section>', '', EXTRA_HTML, flags=re.S)
    features_html = re.sub(r'<section id="client-upload">.*?</section>', '', features_html, flags=re.S)
    extra = extra.replace("status.textContent='Message sent. A confirmation email is on its way.';form.reset()", "status.textContent='Message sent. Reference: '+result.reference;const link=document.createElement('a');link.href=result.quote_url;link.download='';link.textContent=' Download your project quote';status.appendChild(link);form.reset()")
    page = HTML.replace('<a href="#contact">Contact</a>', '<a href="#contact">Contact</a><a href="/client-portal.html">Client Portal</a>')
    return page.replace("https://vimpulseinc.ca/og-image.svg", image_url).replace("</main>", quiz_html + features_html + extra + "</main>")

class Handler(SimpleHTTPRequestHandler):
    def copyfile(self, source, outputfile):
        try:
            shutil.copyfileobj(source, outputfile)
        except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
            pass

    def do_POST(self):
        request_path = self.path.split("?", 1)[0].rstrip("/") or "/"
        if request_path not in {"/api/contact", "/api/client-upload", "/api/private-login", "/api/client-login"}:
            self.send_error(404)
            return
        try:
            length = int(self.headers.get("Content-Length", "0"))
            if length > 60 * 1024 * 1024:
                raise ValueError("Upload is too large")
            fields = json.loads(self.rfile.read(length).decode("utf-8"))
            if request_path == "/api/contact":
                reference, quote_url = send_contact_email(fields)
                body = json.dumps({"ok": True, "reference": reference, "quote_url": quote_url}).encode("utf-8")
            elif request_path == "/api/private-login":
                settings = email_settings()
                password = settings.get("PRIVATE_GALLERY_PASSWORD", "")
                if not password or password == "CHANGE_PRIVATE_PASSWORD_HERE" or fields.get("password") != password:
                    raise PermissionError("Incorrect private gallery password")
                token = secrets.token_urlsafe(24)
                PRIVATE_TOKENS[token] = time.time() + 3600
                files = [{"name": path.name, "url": "/api/private-file?token=" + quote(token) + "&name=" + quote(path.name)} for path in private_files()]
                body = json.dumps({"ok": True, "files": files}).encode("utf-8")
            elif request_path == "/api/client-login":
                folder_name = Path(str(fields.get("folder", ""))).name
                folder = CLIENT_ROOT / folder_name
                password_file = folder / "password.txt"
                supplied_password = str(fields.get("password", ""))
                if folder.parent != CLIENT_ROOT or not folder.is_dir() or not password_file.is_file() or supplied_password != password_file.read_text(encoding="utf-8-sig").strip():
                    raise PermissionError("The client folder or password is not correct")
                token = secrets.token_urlsafe(24)
                CLIENT_TOKENS[token] = {"expires": time.time() + 3600, "folder": folder_name}
                files = [{"name": path.name, "url": "/api/client-file?token=" + quote(token) + "&name=" + quote(path.name)} for path in client_media(folder_name)]
                body = json.dumps({"ok": True, "folder": folder_name, "files": files}).encode("utf-8")
            else:
                reference = re.sub(r"[^A-Za-z0-9-]", "", str(fields.get("reference", "")))[:40]
                name = re.sub(r"[^A-Za-z0-9._-]+", "_", Path(str(fields.get("name", "upload"))).name)[:120]
                data = base64.b64decode(str(fields.get("data", "")), validate=True)
                if len(data) > 50 * 1024 * 1024:
                    raise ValueError("Client upload must be 50 MB or smaller")
                if not reference or not data:
                    raise ValueError("Reference number and file are required")
                UPLOADS.mkdir(parents=True, exist_ok=True)
                destination = UPLOADS / (reference + "_client_" + name)
                destination.write_bytes(data)
                body = json.dumps({"ok": True, "name": destination.name}).encode("utf-8")
            status = 200
        except PermissionError as exc:
            body = json.dumps({"ok": False, "error": str(exc)}).encode("utf-8")
            status = 401
        except ValueError as exc:
            body = json.dumps({"ok": False, "error": str(exc)}).encode("utf-8")
            status = 400
        except Exception as exc:
            self.log_message("Contact email failed: %s", exc)
            body = json.dumps({"ok": False, "error": "Email could not be sent. Check the server email settings."}).encode("utf-8")
            status = 500
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        request_path = self.path.split("?", 1)[0].rstrip("/") or "/"
        if request_path == "/api/client-file":
            query = parse_qs(urlparse(self.path).query)
            token = query.get("token", [""])[0]
            name = Path(query.get("name", [""])[0]).name
            session = CLIENT_TOKENS.get(token, {})
            source = CLIENT_ROOT / session.get("folder", "") / name
            if session.get("expires", 0) < time.time() or not source.is_file() or source.parent.parent != CLIENT_ROOT or source.suffix.lower() not in {".jpg", ".jpeg", ".png", ".mp4"}:
                self.send_error(403)
                return
            body = source.read_bytes()
            self.send_response(200)
            self.send_header("Content-Type", mimetypes.guess_type(source.name)[0] or "application/octet-stream")
            self.send_header("Cache-Control", "private, max-age=300")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        if request_path == "/client-portal.html":
            body = b"<!doctype html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>Client Portal - Vimpulse Inc.</title><style>body{margin:0;background:#080808;color:#f4f1eb;font:14px Arial,sans-serif}.wrap{max-width:1080px;margin:auto;padding:8vh 6vw}a{color:#d8ff3e;text-decoration:none;letter-spacing:.14em;text-transform:uppercase;font-size:11px}.eyebrow{color:#92908b;letter-spacing:.2em;text-transform:uppercase;font-size:11px}h1{font-size:clamp(60px,11vw,150px);line-height:.82;letter-spacing:-.08em;margin:28px 0}p{color:#a9a69f;line-height:1.7;max-width:650px}.portal{border:1px solid #292929;background:#111;padding:28px;max-width:760px}input,button{width:100%;padding:16px;margin:7px 0;box-sizing:border-box;font:inherit}input{background:#f4f1eb;border:0}button{background:#d8ff3e;border:0;cursor:pointer;text-transform:uppercase;letter-spacing:.14em;font-size:11px}.status{color:#d8ff3e;min-height:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:18px;margin-top:30px}.grid figure{margin:0;border:1px solid #292929;background:#111;padding:10px}.grid img,.grid video{width:100%;display:block}.grid figcaption{color:#92908b;font-size:10px;letter-spacing:.1em;text-transform:uppercase;padding:12px 2px 4px}</style></head><body><main class='wrap'><a href='/'>VIMPULSE INC. / HOME</a><div class='eyebrow' style='margin-top:90px'>Client portal / private delivery</div><h1>Your project.<br>Your space.</h1><p>Enter the client folder name and the password provided by Vimpulse Inc. You will only see the approved JPG, PNG, and MP4 files inside your private folder.</p><div class='portal'><form id='portal'><input name='folder' placeholder='Client folder name' autocomplete='username' required><input name='password' type='password' placeholder='Client password' autocomplete='current-password' required><button>Open client portal -&gt;</button><p class='status' id='status'></p></form></div><div class='grid' id='grid'></div></main><script>document.getElementById('portal').onsubmit=async e=>{e.preventDefault();const form=e.currentTarget,status=document.getElementById('status'),grid=document.getElementById('grid');status.textContent='Checking access...';grid.innerHTML='';const response=await fetch('/api/client-login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({folder:form.folder.value,password:form.password.value})});const result=await response.json();if(!response.ok||!result.ok){status.textContent=result.error||'Access denied';return}status.textContent='Opened '+result.folder+' / '+result.files.length+' file(s)';grid.innerHTML=result.files.map(file=>{const video=/\\.mp4$/i.test(file.name);return `<figure>${video?`<video src='${file.url}' controls playsinline></video>`:`<img src='${file.url}' alt='Client project file'>`}<figcaption>${file.name}</figcaption></figure>`}).join('')};</script></body></html>"
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        if request_path == "/api/private-file":
            query = parse_qs(urlparse(self.path).query)
            token = query.get("token", [""])[0]
            name = Path(query.get("name", [""])[0]).name
            source = PRIVATE_MEDIA / name
            if PRIVATE_TOKENS.get(token, 0) < time.time() or not source.is_file() or source.parent != PRIVATE_MEDIA:
                self.send_error(403)
                return
            body = source.read_bytes()
            self.send_response(200)
            self.send_header("Content-Type", mimetypes.guess_type(source.name)[0] or "application/octet-stream")
            self.send_header("Cache-Control", "private, max-age=300")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        if request_path == "/admin":
            auth = self.headers.get("Authorization", "")
            expected = email_settings().get("ADMIN_PASSWORD", "")
            valid = False
            if auth.startswith("Basic ") and expected and expected != "CHANGE_ADMIN_PASSWORD_HERE":
                try:
                    decoded = base64.b64decode(auth[6:]).decode("utf-8")
                    valid = decoded == "admin:" + expected
                except Exception:
                    valid = False
            if not valid:
                self.send_response(401)
                self.send_header("WWW-Authenticate", 'Basic realm="Vimpulse Admin"')
                self.end_headers()
                return
            body = admin_page().encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        if request_path == "/private.html":
            body = b"<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>Private Gallery - Vimpulse Inc.</title><style>body{background:#080808;color:#f4f1eb;font:14px Arial;padding:8vw}input,button{padding:14px;margin:6px 0}button{background:#d8ff3e;border:0}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-top:30px}.grid img,.grid video{width:100%}</style></head><body><a href='/'>VIMPULSE INC. / HOME</a><h1>Private gallery.</h1><p>Enter the gallery password.</p><form id='login'><input id='password' type='password' required><button>Open gallery -&gt;</button></form><p id='status'></p><div class='grid' id='grid'></div><script>document.getElementById('login').onsubmit=async e=>{e.preventDefault();const status=document.getElementById('status');const r=await fetch('/api/private-login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:document.getElementById('password').value})});const x=await r.json();if(!r.ok){status.textContent=x.error||'Access denied';return}status.textContent='';document.getElementById('grid').innerHTML=x.files.map(f=>/\\.(mp4|webm|mov)$/i.test(f.name)?`<video src='${f.url}' controls></video>`:`<img src='${f.url}' alt='Private project'>`).join('')}</script></body></html>"
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        if request_path == "/client-portal.html":
            body = b"<!doctype html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>Client Portal - Vimpulse Inc.</title><style>body{margin:0;background:#080808;color:#f4f1eb;font:14px Arial,sans-serif}.wrap{max-width:900px;margin:auto;padding:10vh 6vw}a{color:#d8ff3e;text-decoration:none;letter-spacing:.14em;text-transform:uppercase;font-size:11px}.eyebrow{color:#92908b;letter-spacing:.2em;text-transform:uppercase;font-size:11px}h1{font-size:clamp(60px,11vw,150px);line-height:.82;letter-spacing:-.08em;margin:28px 0}p{color:#a9a69f;line-height:1.7;max-width:620px}.portal{border:1px solid #292929;background:#111;padding:28px;max-width:700px}input,button{width:100%;padding:16px;margin:7px 0;box-sizing:border-box;font:inherit}input{background:#f4f1eb;border:0}button{background:#d8ff3e;border:0;cursor:pointer;text-transform:uppercase;letter-spacing:.14em;font-size:11px}.status{color:#d8ff3e;min-height:20px}</style></head><body><main class='wrap'><a href='/'>VIMPULSE INC. / HOME</a><div class='eyebrow' style='margin-top:90px'>Client portal / secure upload</div><h1>Send the<br>next file.</h1><p>Use the inquiry reference number from your confirmation email to send a larger project file securely to Vimpulse Inc. Files can be up to 50 MB.</p><div class='portal'><form id='portal'><input name='reference' placeholder='Inquiry reference e.g. VMP-1234ABCD' required><input name='file' type='file' required><button>Upload project file -&gt;</button><p class='status' id='status'></p></form></div></main><script>document.getElementById('portal').onsubmit=async e=>{e.preventDefault();const form=e.currentTarget,status=document.getElementById('status'),file=form.file.files[0];if(!file)return;if(file.size>50*1024*1024){status.textContent='Please choose a file smaller than 50 MB.';return}status.textContent='Uploading...';const reader=new FileReader();reader.onload=async()=>{try{const response=await fetch('/api/client-upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({reference:form.reference.value,name:file.name,data:String(reader.result).split(',')[1]})});const result=await response.json();if(!response.ok||!result.ok)throw new Error(result.error||'Upload failed');status.textContent='Upload received: '+result.name;form.reset()}catch(error){status.textContent=error.message}};reader.readAsDataURL(file)}</script></body></html>"
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        simple_pages = {
            "/service-guide.txt": (SERVICE_GUIDE, "text/plain; charset=utf-8"),
            "/sitemap.xml": (SITEMAP, "application/xml; charset=utf-8"),
            "/robots.txt": (ROBOTS, "text/plain; charset=utf-8"),
            "/404.html": (NOT_FOUND_HTML, "text/html; charset=utf-8"),
        }
        if request_path in simple_pages:
            content, content_type = simple_pages[request_path]
            body = content.encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", content_type)
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        if request_path == "/privacy.html":
            body = PRIVACY_HTML.encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        if request_path == "/og-image.svg":
            body = OG_SVG.encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "image/svg+xml")
            self.send_header("Cache-Control", "public, max-age=3600")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        if request_path == "/api/media":
            body = json.dumps(payload()).encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Cache-Control", "no-store")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        if request_path in ("/", "/index.html", "/website", "/website/index.html"):
            body = share_html().encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Cache-Control", "no-store, max-age=0")
            self.send_header("Vary", "User-Agent")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        translated = Path(self.translate_path(request_path))
        if not translated.exists():
            body = NOT_FOUND_HTML.encode("utf-8")
            self.send_response(404)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        super().do_GET()

    def log_message(self, fmt, *args):
        print("[%s] %s" % (self.log_date_time_string(), fmt % args))

os.chdir(BASE)
print("VimpulseInc server running on http://127.0.0.1:%d" % PORT)
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()

