2 Коммиты c705429e89 ... b583c8fa29

Автор SHA1 Сообщение Дата
  parkeradmin b583c8fa29 Merge branch 'development' into production 1 месяц назад
  parkeradmin e36a93d761 Redesign junior desktop support portfolio 1 месяц назад
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.
 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.
 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
 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'
 mariadb <<'SQL'
 CREATE DATABASE IF NOT EXISTS desktop_support
 CREATE DATABASE IF NOT EXISTS desktop_support
   CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
   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>
 <!doctype html>
 <html lang="en">
 <html lang="en">
 <head>
 <head>
     <meta charset="utf-8">
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <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>
 </head>
 <body>
 <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>
     </section>
 </main>
 </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>
 </body>
 </html>
 </html>

Некоторые файлы не были показаны из-за большого количества измененных файлов