浏览代码

Redesign junior desktop support portfolio

parkeradmin 1 月之前
父节点
当前提交
e36a93d761
共有 6 个文件被更改,包括 403 次插入38 次删除
  1. 4 0
      README.md
  2. 10 0
      provision/bootstrap.sh
  3. 2 0
      public/assets/site.css
  4. 91 0
      public/assets/site.js
  5. 123 0
      public/contact.php
  6. 173 38
      public/index.php

+ 4 - 0
README.md

@@ -47,3 +47,7 @@ vagrant destroy
 These credentials are for local development only. Do not reuse them in production.
 
 Apache logs are stored in `/var/log/apache2/desktop-support-*.log` inside the VM.
+
+## Contact form
+
+The contact endpoint sends through Resend without exposing its API key to the browser. For local development, add `RESEND_KEY=re_...` to the ignored `.env` file and run `vagrant provision`. Production reads `/etc/desktop-resume/contact.env`, which should be readable only by `root` and the web-server group.

+ 10 - 0
provision/bootstrap.sh

@@ -58,6 +58,16 @@ ln -sf /etc/php/development-overrides.ini "/etc/php/${PHP_VERSION}/cli/conf.d/99
 
 systemctl enable --now mariadb apache2
 
+install -d -m 0750 -o root -g www-data /etc/desktop-resume
+if [ -f /vagrant/.env ]; then
+  RESEND_KEY="$(sed -n 's/^RESEND_KEY=//p' /vagrant/.env | head -n 1 | tr -d '\r')"
+  if [ -n "$RESEND_KEY" ]; then
+    printf 'RESEND_API_KEY=%s\nRESEND_FROM=%s\n' "$RESEND_KEY" 'Desktop Support Portfolio <contact@jadenportfolio.com>' >/etc/desktop-resume/contact.env
+    chown root:www-data /etc/desktop-resume/contact.env
+    chmod 0640 /etc/desktop-resume/contact.env
+  fi
+fi
+
 mariadb <<'SQL'
 CREATE DATABASE IF NOT EXISTS desktop_support
   CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

文件差异内容过多而无法显示
+ 2 - 0
public/assets/site.css


+ 91 - 0
public/assets/site.js

@@ -0,0 +1,91 @@
+(() => {
+  const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
+  document.getElementById('year').textContent = new Date().getFullYear();
+
+  document.getElementById('ticket-number').textContent = `TKT-${String(Math.floor(1000 + Math.random() * 9000))}`;
+  document.getElementById('started-at').value = Math.floor(Date.now() / 1000);
+
+  const reveals = document.querySelectorAll('.reveal');
+  if (reducedMotion || !('IntersectionObserver' in window)) {
+    reveals.forEach((item) => item.classList.add('visible'));
+  } else {
+    const observer = new IntersectionObserver((entries) => {
+      entries.forEach((entry) => {
+        if (entry.isIntersecting) {
+          entry.target.classList.add('visible');
+          observer.unobserve(entry.target);
+        }
+      });
+    }, { threshold: 0.12 });
+    reveals.forEach((item) => observer.observe(item));
+  }
+
+  const form = document.getElementById('contact-form');
+  const status = document.getElementById('form-status');
+  form.addEventListener('submit', async (event) => {
+    event.preventDefault();
+    if (!form.reportValidity()) return;
+    form.classList.remove('error', 'success');
+    form.classList.add('sending');
+    status.textContent = 'Routing your ticket…';
+    form.querySelector('.button-label').textContent = 'Sending';
+    try {
+      const response = await fetch(form.action, { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } });
+      const result = await response.json();
+      if (!response.ok || !result.ok) throw new Error(result.message || 'Message could not be sent.');
+      form.classList.add('success');
+      status.textContent = result.message;
+      form.reset();
+      document.getElementById('started-at').value = Math.floor(Date.now() / 1000);
+      form.querySelector('.button-label').textContent = 'Ticket received ✓';
+    } catch (error) {
+      form.classList.add('error');
+      status.textContent = error.message;
+      form.querySelector('.button-label').textContent = 'Try again';
+    } finally {
+      form.classList.remove('sending');
+    }
+  });
+
+  if (reducedMotion) return;
+  const canvas = document.getElementById('particle-field');
+  const context = canvas.getContext('2d');
+  const pointer = { x: -1000, y: -1000 };
+  let particles = [];
+  let width = 0;
+  let height = 0;
+  const resize = () => {
+    const ratio = Math.min(devicePixelRatio || 1, 2);
+    width = innerWidth;
+    height = innerHeight;
+    canvas.width = width * ratio;
+    canvas.height = height * ratio;
+    canvas.style.width = `${width}px`;
+    canvas.style.height = `${height}px`;
+    context.setTransform(ratio, 0, 0, ratio, 0, 0);
+    particles = Array.from({ length: Math.min(55, Math.floor(width / 24)) }, () => ({
+      x: Math.random() * width, y: Math.random() * height, vx: (Math.random() - .5) * .18, vy: (Math.random() - .5) * .18, r: Math.random() * 1.4 + .4
+    }));
+  };
+  addEventListener('resize', resize, { passive: true });
+  addEventListener('pointermove', (event) => { pointer.x = event.clientX; pointer.y = event.clientY; }, { passive: true });
+  addEventListener('pointerleave', () => { pointer.x = pointer.y = -1000; });
+  resize();
+  const draw = () => {
+    context.clearRect(0, 0, width, height);
+    particles.forEach((particle, index) => {
+      particle.x += particle.vx; particle.y += particle.vy;
+      if (particle.x < 0 || particle.x > width) particle.vx *= -1;
+      if (particle.y < 0 || particle.y > height) particle.vy *= -1;
+      const pdx = pointer.x - particle.x, pdy = pointer.y - particle.y, pointerDistance = Math.hypot(pdx, pdy);
+      if (pointerDistance < 130) { particle.x -= pdx * .0008; particle.y -= pdy * .0008; }
+      context.beginPath(); context.arc(particle.x, particle.y, particle.r, 0, Math.PI * 2); context.fillStyle = '#6fc9ff55'; context.fill();
+      for (let j = index + 1; j < particles.length; j++) {
+        const other = particles[j], distance = Math.hypot(particle.x - other.x, particle.y - other.y);
+        if (distance < 115) { context.beginPath(); context.moveTo(particle.x, particle.y); context.lineTo(other.x, other.y); context.strokeStyle = `rgba(70,185,255,${(1 - distance / 115) * .09})`; context.stroke(); }
+      }
+    });
+    requestAnimationFrame(draw);
+  };
+  draw();
+})();

+ 123 - 0
public/contact.php

@@ -0,0 +1,123 @@
+<?php
+declare(strict_types=1);
+
+header('Content-Type: application/json; charset=utf-8');
+header('Cache-Control: no-store');
+
+function respond(int $status, bool $ok, string $message): never
+{
+    http_response_code($status);
+    echo json_encode(['ok' => $ok, 'message' => $message], JSON_UNESCAPED_SLASHES);
+    exit;
+}
+
+function loadContactEnvironment(): array
+{
+    $values = [];
+    foreach (['/etc/desktop-resume/contact.env', '/vagrant/.env'] as $path) {
+        if (!is_readable($path)) {
+            continue;
+        }
+        foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
+            $line = trim($line);
+            if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) {
+                continue;
+            }
+            [$key, $value] = array_map('trim', explode('=', $line, 2));
+            $values[$key] = trim($value, "\"'");
+        }
+    }
+    return $values;
+}
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+    respond(405, false, 'Please submit the contact form.');
+}
+
+$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
+if (str_contains($contentType, 'application/json')) {
+    $input = json_decode(file_get_contents('php://input') ?: '{}', true);
+    $input = is_array($input) ? $input : [];
+} else {
+    $input = $_POST;
+}
+
+if (trim((string) ($input['website'] ?? '')) !== '') {
+    respond(200, true, 'Thanks—your ticket is in the queue.');
+}
+
+$startedAt = (int) ($input['started_at'] ?? 0);
+if ($startedAt <= 0 || time() - $startedAt < 3 || time() - $startedAt > 86400) {
+    respond(400, false, 'Please refresh the page and try again.');
+}
+
+$name = trim((string) ($input['name'] ?? ''));
+$email = trim((string) ($input['email'] ?? ''));
+$topic = trim((string) ($input['topic'] ?? ''));
+$message = trim((string) ($input['message'] ?? ''));
+$allowedTopics = ['Junior support opportunity', 'Interview conversation', 'Networking', 'Website feedback'];
+
+if ($name === '' || mb_strlen($name) > 80) {
+    respond(422, false, 'Please enter your name.');
+}
+if (!filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email) > 160) {
+    respond(422, false, 'Please enter a valid email address.');
+}
+if (!in_array($topic, $allowedTopics, true)) {
+    respond(422, false, 'Please choose a ticket type.');
+}
+if (mb_strlen($message) < 20 || mb_strlen($message) > 3000) {
+    respond(422, false, 'Please include a little more detail (20–3,000 characters).');
+}
+
+$rateKey = hash('sha256', ($_SERVER['REMOTE_ADDR'] ?? 'unknown') . '|desktop-contact');
+$rateFile = sys_get_temp_dir() . '/contact-' . $rateKey;
+$lastSent = is_file($rateFile) ? (int) file_get_contents($rateFile) : 0;
+if ($lastSent > time() - 60) {
+    respond(429, false, 'That ticket just came through. Please wait a minute before sending another.');
+}
+
+$environment = loadContactEnvironment();
+$apiKey = getenv('RESEND_API_KEY') ?: ($environment['RESEND_API_KEY'] ?? $environment['RESEND_KEY'] ?? '');
+$from = getenv('RESEND_FROM') ?: ($environment['RESEND_FROM'] ?? 'Desktop Support Portfolio <contact@jadenportfolio.com>');
+
+if ($apiKey === '') {
+    error_log('Contact form: RESEND_API_KEY is not configured.');
+    respond(503, false, 'The message desk is temporarily offline. Please email me directly instead.');
+}
+
+$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+$safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+$safeTopic = htmlspecialchars($topic, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+$safeMessage = nl2br(htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
+$ticket = 'WEB-' . strtoupper(substr(hash('sha256', $email . microtime()), 0, 8));
+
+$payload = [
+    'from' => $from,
+    'to' => ['praesidium909@gmail.com'],
+    'reply_to' => $email,
+    'subject' => "[$ticket] $topic — $name",
+    'html' => "<div style=\"font-family:Arial,sans-serif;max-width:640px;margin:auto;color:#172033\"><p style=\"color:#64748b\">New portfolio support ticket · <strong>$ticket</strong></p><h1 style=\"font-size:24px\">$safeTopic</h1><table style=\"width:100%;border-collapse:collapse;margin:24px 0\"><tr><td style=\"padding:10px;border-bottom:1px solid #e2e8f0;color:#64748b\">From</td><td style=\"padding:10px;border-bottom:1px solid #e2e8f0\"><strong>$safeName</strong></td></tr><tr><td style=\"padding:10px;border-bottom:1px solid #e2e8f0;color:#64748b\">Email</td><td style=\"padding:10px;border-bottom:1px solid #e2e8f0\">$safeEmail</td></tr></table><div style=\"background:#f8fafc;border-left:4px solid #0ea5e9;padding:20px;line-height:1.6\">$safeMessage</div><p style=\"color:#64748b;font-size:13px;margin-top:24px\">Reply directly to this email to respond.</p></div>",
+    'text' => "Ticket: $ticket\nType: $topic\nFrom: $name <$email>\n\n$message",
+];
+
+$curl = curl_init('https://api.resend.com/emails');
+curl_setopt_array($curl, [
+    CURLOPT_POST => true,
+    CURLOPT_RETURNTRANSFER => true,
+    CURLOPT_TIMEOUT => 15,
+    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey, 'Content-Type: application/json'],
+    CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_SLASHES),
+]);
+$response = curl_exec($curl);
+$status = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
+$error = curl_error($curl);
+curl_close($curl);
+
+if ($response === false || $status < 200 || $status >= 300) {
+    error_log("Contact form: Resend failed with HTTP $status: $error");
+    respond(502, false, 'The ticket could not be sent right now. Please use the email link instead.');
+}
+
+file_put_contents($rateFile, (string) time(), LOCK_EX);
+respond(200, true, "Ticket $ticket received. I’ll be in touch soon!");

+ 173 - 38
public/index.php

@@ -1,49 +1,184 @@
-<?php
-declare(strict_types=1);
-
-$databaseStatus = 'Unavailable';
-
-try {
-    $database = new PDO(
-        'mysql:host=localhost;dbname=desktop_support;charset=utf8mb4',
-        'desktop_support',
-        'development_only',
-        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
-    );
-    $databaseStatus = 'Connected';
-} catch (PDOException $exception) {
-    $databaseStatus = 'Unavailable';
-}
-?>
+<?php declare(strict_types=1); ?>
 <!doctype html>
 <html lang="en">
 <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
-    <title>Desktop Support Portfolio</title>
-    <style>
-        :root { color-scheme: dark; font-family: system-ui, sans-serif; }
-        body { margin: 0; background: #0b1220; color: #e5edf7; }
-        main { width: min(720px, calc(100% - 2rem)); margin: 12vh auto; }
-        .card { padding: 2rem; border: 1px solid #26364d; border-radius: 18px; background: #111c2e; box-shadow: 0 24px 70px #0006; }
-        h1 { margin-top: 0; font-size: clamp(2rem, 8vw, 4rem); line-height: 1; }
-        p { color: #b8c6d9; line-height: 1.65; }
-        dl { display: grid; grid-template-columns: max-content 1fr; gap: .75rem 1.25rem; margin: 2rem 0 0; }
-        dt { color: #7dd3fc; } dd { margin: 0; }
-        .ok { color: #86efac; }
-    </style>
+    <meta name="description" content="Junior desktop support specialist portfolio focused on approachable troubleshooting, user support, Windows, Microsoft 365, hardware, networking, and hands-on learning.">
+    <meta name="theme-color" content="#07111f">
+    <title>Junior Desktop Support | Jaden Portfolio</title>
+    <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=DM+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
+    <link rel="stylesheet" href="/assets/site.css">
+    <script src="/assets/site.js" defer></script>
 </head>
 <body>
-<main>
-    <section class="card">
-        <h2>Desktop support,<br>done right!</h2>
-        <p>Your Debian 13 LAMP development environment is ready. Replace this page with your experience, certifications, projects, and contact information.</p>
-        <dl>
-            <dt>PHP</dt><dd><?= htmlspecialchars(PHP_VERSION, ENT_QUOTES) ?></dd>
-            <dt>MariaDB</dt><dd class="<?= $databaseStatus === 'Connected' ? 'ok' : '' ?>"><?= htmlspecialchars($databaseStatus, ENT_QUOTES) ?></dd>
-            <dt>Apache</dt><dd class="ok">Running</dd>
-        </dl>
+<canvas id="particle-field" aria-hidden="true"></canvas>
+<div class="noise" aria-hidden="true"></div>
+
+<header class="site-header">
+    <a class="brand" href="#top" aria-label="Back to top">
+        <span class="brand-mark">JP</span>
+        <span>Support Desk</span>
+    </a>
+    <nav aria-label="Primary navigation">
+        <a href="#skills">Skills</a>
+        <a href="#approach">Approach</a>
+        <a class="nav-cta" href="#contact">Open a ticket</a>
+    </nav>
+</header>
+
+<main id="top">
+    <section class="hero section-shell">
+        <div class="hero-copy reveal">
+            <div class="availability"><span></span> Ready for a junior support role</div>
+            <p class="eyebrow">Junior Desktop Support Specialist</p>
+            <h1>Technical support with a <em>human signal.</em></h1>
+            <p class="hero-lede">I turn “it stopped working” into calm, clear next steps. I’m building a career around patient troubleshooting, dependable follow-through, and making technology feel less intimidating.</p>
+            <div class="hero-actions">
+                <a class="button button-primary" href="#contact">Let’s troubleshoot together <span>↗</span></a>
+                <a class="button button-ghost" href="#skills">Explore my toolkit</a>
+            </div>
+            <div class="quick-stats" aria-label="Portfolio highlights">
+                <div><strong>01</strong><span>User-first mindset</span></div>
+                <div><strong>02</strong><span>Hands-on home lab</span></div>
+                <div><strong>03</strong><span>Always documenting</span></div>
+            </div>
+        </div>
+
+        <div class="hero-console reveal" aria-label="Example support workflow">
+            <div class="console-top">
+                <div class="console-dots"><i></i><i></i><i></i></div>
+                <span>support_session.log</span>
+                <span class="live-pill">LIVE</span>
+            </div>
+            <div class="console-body">
+                <div class="ticket-meta"><span>INC-0027</span><span class="priority">NORMAL</span></div>
+                <p class="console-prompt"><span>user@desk:~$</span> My laptop can’t connect to Wi-Fi.</p>
+                <ol class="diagnostic-list">
+                    <li class="complete"><i>✓</i><span><b>Listen first</b><small>Confirm impact and recent changes</small></span></li>
+                    <li class="complete"><i>✓</i><span><b>Isolate the cause</b><small>Adapter, network, credentials, or DNS</small></span></li>
+                    <li class="active"><i></i><span><b>Apply & verify</b><small>Restore service and test with the user</small></span></li>
+                    <li><i>4</i><span><b>Document clearly</b><small>Leave the next technician a useful trail</small></span></li>
+                </ol>
+                <div class="resolution"><span>Resolution confidence</span><strong>92%</strong><div><i></i></div></div>
+            </div>
+        </div>
+    </section>
+
+    <section class="ticker" aria-label="Core skills">
+        <div><span>Windows 10/11</span><i>✦</i><span>Microsoft 365</span><i>✦</i><span>Hardware Support</span><i>✦</i><span>Active Directory Fundamentals</span><i>✦</i><span>Networking Basics</span><i>✦</i><span>Remote Support</span></div>
+    </section>
+
+    <section id="skills" class="section-shell skills-section">
+        <div class="section-heading reveal">
+            <p class="eyebrow">My growing toolkit</p>
+            <h2>Strong foundations.<br><span>Curious by default.</span></h2>
+            <p>I’m early in my IT career and serious about the fundamentals—the everyday skills that keep people productive and earn their trust.</p>
+        </div>
+        <div class="skill-grid">
+            <article class="skill-card featured reveal">
+                <div class="skill-icon">⌘</div><span class="skill-level">CORE FOCUS</span>
+                <h3>Desktop troubleshooting</h3>
+                <p>Methodical diagnosis for Windows issues, application errors, peripherals, updates, profiles, and everyday workstation problems.</p>
+                <ul><li>Windows 10 & 11</li><li>Drivers & updates</li><li>Printers & peripherals</li></ul>
+            </article>
+            <article class="skill-card reveal">
+                <div class="skill-icon">◎</div><span class="skill-level">BUILDING</span>
+                <h3>Identity & access</h3>
+                <p>Practicing the account lifecycle basics that help users get secure access without unnecessary friction.</p>
+                <ul><li>Active Directory basics</li><li>Password resets</li><li>Groups & permissions</li></ul>
+            </article>
+            <article class="skill-card reveal">
+                <div class="skill-icon">↯</div><span class="skill-level">PRACTICED</span>
+                <h3>Hardware support</h3>
+                <p>Comfortable identifying components, checking connections, replacing common parts, and narrowing down hardware faults.</p>
+                <ul><li>PC components</li><li>Workstation setup</li><li>Preventive care</li></ul>
+            </article>
+            <article class="skill-card reveal">
+                <div class="skill-icon">⌁</div><span class="skill-level">FOUNDATIONAL</span>
+                <h3>Networking</h3>
+                <p>Working knowledge of the path between a device and its destination—and the tools to find where that path breaks.</p>
+                <ul><li>TCP/IP & DNS</li><li>Wi-Fi troubleshooting</li><li>ipconfig, ping & tracert</li></ul>
+            </article>
+            <article class="skill-card reveal">
+                <div class="skill-icon">☁</div><span class="skill-level">LEARNING</span>
+                <h3>Microsoft 365</h3>
+                <p>Supporting the productivity tools people rely on, from sign-in and Outlook basics to Teams and OneDrive sync issues.</p>
+                <ul><li>Outlook & Teams</li><li>OneDrive basics</li><li>Account support</li></ul>
+            </article>
+        </div>
+    </section>
+
+    <section id="approach" class="approach-section">
+        <div class="section-shell approach-inner">
+            <div class="approach-copy reveal">
+                <p class="eyebrow">How I work the problem</p>
+                <h2>No mystery fixes.<br><span>Just a clear process.</span></h2>
+                <p>Great junior support isn’t about knowing every answer. It’s about asking useful questions, testing carefully, communicating honestly, and learning from every ticket.</p>
+                <div class="principles">
+                    <span>Patient with people</span><span>Careful with systems</span><span>Hungry to learn</span>
+                </div>
+            </div>
+            <div class="process" role="list">
+                <article class="process-step reveal" role="listitem"><span>01</span><div><h3>Understand</h3><p>Listen without assumptions. Translate symptoms into a clear, shared problem statement.</p></div></article>
+                <article class="process-step reveal" role="listitem"><span>02</span><div><h3>Investigate</h3><p>Start simple, gather evidence, and change one thing at a time so the result means something.</p></div></article>
+                <article class="process-step reveal" role="listitem"><span>03</span><div><h3>Resolve</h3><p>Fix the root cause when possible, test the outcome, and confirm the user is truly back to work.</p></div></article>
+                <article class="process-step reveal" role="listitem"><span>04</span><div><h3>Improve</h3><p>Document the path, share what worked, and add the lesson to my growing support playbook.</p></div></article>
+            </div>
+        </div>
+    </section>
+
+    <section class="lab-section section-shell">
+        <div class="lab-card reveal">
+            <div class="lab-copy">
+                <p class="eyebrow">Currently in the lab</p>
+                <h2>I learn by building the thing.</h2>
+                <p>This portfolio runs on infrastructure I configured myself: Debian 13, Apache, MariaDB, PHP, Git-based deployments, webhooks, and automated TLS.</p>
+            </div>
+            <div class="lab-stack" aria-label="Technologies used for this portfolio">
+                <span>Debian 13 <i>online</i></span><span>Apache <i>serving</i></span><span>MariaDB <i>connected</i></span><span>Git + Gogs <i>deployed</i></span><span>Let's Encrypt <i>secure</i></span><span>PHP 8.4 <i>running</i></span>
+            </div>
+        </div>
+    </section>
+
+    <section id="contact" class="contact-section section-shell">
+        <div class="contact-copy reveal">
+            <p class="eyebrow">Start a conversation</p>
+            <h2>Got an opportunity?<br><span>Open a ticket.</span></h2>
+            <p>Hiring for a junior support role, building an IT team, or just want to compare troubleshooting notes? Send a message—no hold music required.</p>
+            <a class="email-link" href="mailto:praesidium909@gmail.com"><span>@</span><div><small>Prefer regular email?</small><b>praesidium909@gmail.com</b></div></a>
+        </div>
+
+        <form id="contact-form" class="contact-form reveal" action="/contact.php" method="post" novalidate>
+            <div class="form-top"><div><span class="status-dot"></span> NEW SUPPORT REQUEST</div><span id="ticket-number">TKT-0000</span></div>
+            <input class="honey" type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true">
+            <input type="hidden" name="started_at" id="started-at" value="">
+            <div class="form-row">
+                <label><span>Your name</span><input type="text" name="name" autocomplete="name" required maxlength="80" placeholder="How should I address you?"></label>
+                <label><span>Your email</span><input type="email" name="email" autocomplete="email" required maxlength="160" placeholder="you@company.com"></label>
+            </div>
+            <label><span>What brings you here?</span>
+                <select name="topic" required>
+                    <option value="" selected disabled>Choose a ticket type...</option>
+                    <option value="Junior support opportunity">Junior support opportunity</option>
+                    <option value="Interview conversation">Interview conversation</option>
+                    <option value="Networking">Networking / say hello</option>
+                    <option value="Website feedback">Website feedback</option>
+                </select>
+            </label>
+            <label><span>Ticket details</span><textarea name="message" required minlength="20" maxlength="3000" rows="5" placeholder="Tell me about the role, team, or idea..."></textarea></label>
+            <div class="form-footer">
+                <p id="form-status" role="status" aria-live="polite">Typical response: within one business day</p>
+                <button class="button button-primary" type="submit"><span class="button-label">Submit ticket</span><span class="send-icon">↗</span></button>
+            </div>
+        </form>
     </section>
 </main>
+
+<footer>
+    <div class="section-shell"><div><span class="brand-mark">JP</span><p>Junior support. Serious follow-through.</p></div><p>Designed, built & deployed with curiosity. <span id="year"></span></p></div>
+</footer>
 </body>
 </html>

部分文件因为文件数量过多而无法显示