commit e13a69f85aab399b7cb53406dfa0454aeb48d333 Author: Jason Eugene Garrison Date: Sat Jul 11 08:17:34 2026 -0500 Import current resume static site diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..377524c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.old/ +tmp/ +Microsoft.PowerShell.Core/ diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..18206e3 --- /dev/null +++ b/.htaccess @@ -0,0 +1,4 @@ +RewriteEngine On + +RedirectMatch 301 ^/(blog|blog-full-post|contact|cover-latter|portfolio-full|project|test|index1|index2|old_index)\.html$ / +RedirectMatch 301 ^/process\.php$ / diff --git a/README.md b/README.md new file mode 100644 index 0000000..b97011f --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# jasoneugenegarrison.com + +Static resume site for Jason Eugene Garrison. + +## Workflow + +Use Git for normal rollback and history. + +If a risky manual edit needs a temporary snapshot, place it in an external archive safe outside the repo after the matching Git commit is pushed. + +## Legacy helper scripts + +The site currently includes helper scripts from the pre-Git workflow. Prefer Git commits for routine changes. If you need the legacy snapshot helper, move resulting `.old` output to the external archive safe instead of keeping it in the repo. + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\save-old.ps1 -Label "before-homepage-edit" +``` + +After editing public files, refresh the permissions nginx needs: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\publish-permissions.ps1 +``` + +To rebuild the downloadable resume files from the current site: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build-resume-assets.ps1 +``` + +Snapshots are ignored by Git and should not be retained in the repo. diff --git a/Resume_Jason_Eugene_Garrison.docx b/Resume_Jason_Eugene_Garrison.docx new file mode 100644 index 0000000..d16d39e Binary files /dev/null and b/Resume_Jason_Eugene_Garrison.docx differ diff --git a/Resume_Jason_Eugene_Garrison.pdf b/Resume_Jason_Eugene_Garrison.pdf new file mode 100644 index 0000000..9ce3dd4 Binary files /dev/null and b/Resume_Jason_Eugene_Garrison.pdf differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..8deed12 --- /dev/null +++ b/index.html @@ -0,0 +1,172 @@ + + + + + + Jason E. Garrison | IT Infrastructure Specialist + + + + + + + + + + +
+
+ + + PDF + + + + DOCX + + + +
+ +
+ Jason Garrison +
+

Jason Eugene Garrison

+

IT Infrastructure Specialist & Field Engineer

+

Available for infrastructure support, field engineering, and commercial IT operations.

+ +
+ Oak Grove, KY + + + + + + + + + + + + + + + www.jasoneugenegarrison.com + +
+
+
+ +
+
+

Professional Profile

+

+ Field-tested IT Infrastructure Specialist with more than 20 years of hands-on experience in network deployment, hardware diagnostics, and commercial support. Known for solving field problems under pressure in healthcare, correctional, commercial, and customer-facing environments. +

+
+ Field Engineering + Network Deployment + Hardware Diagnostics + Commercial Support + Project Execution + Client Communication +
+
+ +
+

Core Tools & Work Types

+

+ Network drops, hardware swaps, workstation repair, site surveys, MSP tickets, printer support, router and switch installs, customer handoff, and field documentation. +

+
+ +
+

Professional Experience

+ +
+
+

Independent Commercial IT Specialist

+

2019 - Present

+
+

Independent Contractor | Tennessee & Kentucky

+
    +
  • Serve as a field engineer for national MSPs and direct commercial clients, handling technical tickets in healthcare facilities, correctional institutions, and other sensitive work sites.
  • +
  • Manage infrastructure projects from site assessment through installation, testing, and handoff.
  • +
  • Review scopes, cabling paths, and equipment needs to reduce waste while keeping systems reliable and serviceable.
  • +
+
+ +
+
+

Owner & Lead Technician

+

2014 - 2019

+
+

Garrison Tech Services | Nashville, TN

+
    +
  • Founded and operated an independent computer repair and support business serving residential and small-business customers.
  • +
  • Built a profitable service operation through fast turnaround, clear communication, and repeat referrals.
  • +
  • Diagnosed and repaired thousands of machines while managing pricing, inventory, customer service, and local marketing.
  • +
+
+ +
+
+

Telecommunications Contractor

+

2011 - 2014

+
+

ISP Infrastructure Contractors | Indiana

+
    +
  • Completed end-to-end ISP installations, including line work, customer-premise setup, and in-home network configuration.
  • +
  • Balanced technical installation work with customer-facing sales and service package upgrades.
  • +
+
+ +
+
+

Graphic Design & IT Consultant

+

2004 - 2009

+
+

Grace Evangelistic Ministries | Nashville, TN

+
    +
  • Designed and prepared a bi-monthly donor newsletter, coordinating layout, production, and distribution.
  • +
  • Supported office computers, printers, and internal systems so administrative staff could work without avoidable downtime.
  • +
+
+
+ +
+

Education & Background

+
+

Self-Directed Professional Development

+

Hands-on technical training through field work, repair, deployment, and independent study.

+

+ My technical education has been built through direct experience: diagnosing failures, learning unfamiliar systems under pressure, and turning difficult field problems into working solutions. +

+

+ That background has made me resourceful, self-directed, and comfortable learning new tools without needing a formal classroom before I can contribute. +

+
+
+ +
+

Career Target

+

+ After more than two decades of independent technical work, I am ready for a dedicated long-term role where I can bring steady judgment, practical troubleshooting, and strong field ownership to a team. I am looking for an organization that values independent troubleshooting, clear communication, and practical ownership. +

+
+
+ +
+ © 2026 Jason Eugene Garrison + Last updated June 15, 2026 +
+
+ + + + diff --git a/jason.JPG b/jason.JPG new file mode 100644 index 0000000..c7566b7 Binary files /dev/null and b/jason.JPG differ diff --git a/jason2026.jpg b/jason2026.jpg new file mode 100644 index 0000000..e5ccf7a Binary files /dev/null and b/jason2026.jpg differ diff --git a/scripts/build-resume-assets.ps1 b/scripts/build-resume-assets.ps1 new file mode 100644 index 0000000..32a6f7c --- /dev/null +++ b/scripts/build-resume-assets.ps1 @@ -0,0 +1,73 @@ +$ErrorActionPreference = "Stop" + +$siteRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).ProviderPath +$python = "C:\Users\jason\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe" +$chrome = "C:\Program Files\Google\Chrome\Application\chrome.exe" +$port = 8793 +$pdfPath = Join-Path $siteRoot "Resume_Jason_Eugene_Garrison.pdf" +$docxPath = Join-Path $siteRoot "Resume_Jason_Eugene_Garrison.docx" + +if (-not (Test-Path $python)) { + throw "Bundled Python was not found at $python" +} + +if (-not (Test-Path $chrome)) { + throw "Chrome was not found at $chrome" +} + +& $python (Join-Path $PSScriptRoot "generate-resume-docx.py") $siteRoot $docxPath +if ($LASTEXITCODE -ne 0) { + throw "DOCX generation failed with exit code $LASTEXITCODE" +} + +$server = $null +try { + $server = Start-Process -FilePath $python ` + -ArgumentList @("-m", "http.server", "$port", "--bind", "127.0.0.1", "--directory", $siteRoot) ` + -WorkingDirectory $env:TEMP ` + -WindowStyle Hidden ` + -PassThru + + $url = "http://127.0.0.1:$port/index.html?print=$(Get-Date -Format yyyyMMddHHmmss)" + + $ready = $false + for ($i = 0; $i -lt 30; $i++) { + try { + $response = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 2 + if ($response.StatusCode -eq 200 -and $response.Content -match "Jason Eugene Garrison") { + $ready = $true + break + } + } catch { + Start-Sleep -Milliseconds 500 + } + } + + if (-not $ready) { + throw "Local resume preview server was not reachable at $url" + } + + & $chrome ` + --headless=new ` + --disable-gpu ` + --disable-background-networking ` + --disable-sync ` + --metrics-recording-only ` + --no-pdf-header-footer ` + --print-to-pdf="$pdfPath" ` + $url | Out-Null + + if ($LASTEXITCODE -ne 0) { + throw "PDF generation failed with exit code $LASTEXITCODE" + } +} +finally { + if ($server -and -not $server.HasExited) { + Stop-Process -Id $server.Id -Force + } +} + +& (Join-Path $PSScriptRoot "publish-permissions.ps1") + +Write-Host "Updated $pdfPath" +Write-Host "Updated $docxPath" diff --git a/scripts/generate-resume-docx.py b/scripts/generate-resume-docx.py new file mode 100644 index 0000000..1712851 --- /dev/null +++ b/scripts/generate-resume-docx.py @@ -0,0 +1,214 @@ +from pathlib import Path +import html +import re +import sys + +from docx import Document +from docx.enum.section import WD_SECTION +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.oxml import OxmlElement +from docx.oxml.ns import qn +from docx.shared import Inches, Pt, RGBColor +from lxml import html as lxml_html + + +CONTACTS = { + "Location": "Oak Grove, KY", + "Phone (KY)": "(270) 889-8072", + "Phone (TN)": "(865) 985-1641", + "Email": "jasoneugenegarrison@icloud.com", + "Website": "www.jasoneugenegarrison.com", +} + + +def clean(text): + return re.sub(r"\s+", " ", html.unescape(text or "")).strip() + + +def by_class(node, class_name, tag="*"): + xpath = f".//{tag}[contains(concat(' ', normalize-space(@class), ' '), ' {class_name} ')]" + return node.xpath(xpath) + + +def add_bottom_border(paragraph, color="b91c1c", size="12"): + p_pr = paragraph._p.get_or_add_pPr() + p_bdr = OxmlElement("w:pBdr") + bottom = OxmlElement("w:bottom") + bottom.set(qn("w:val"), "single") + bottom.set(qn("w:sz"), size) + bottom.set(qn("w:space"), "4") + bottom.set(qn("w:color"), color) + p_bdr.append(bottom) + p_pr.append(p_bdr) + + +def set_run(run, size=None, bold=None, color=None, all_caps=False): + if size: + run.font.size = Pt(size) + if bold is not None: + run.bold = bold + if color: + run.font.color.rgb = RGBColor.from_string(color) + if all_caps: + run.text = run.text.upper() + + +def add_heading(doc, text): + p = doc.add_paragraph() + p.paragraph_format.space_before = Pt(12) + p.paragraph_format.space_after = Pt(6) + run = p.add_run(text.upper()) + run.bold = True + run.font.name = "Arial" + run.font.size = Pt(11) + run.font.color.rgb = RGBColor(185, 28, 28) + add_bottom_border(p, "e5e7eb", "6") + return p + + +def add_bullet(doc, text): + p = doc.add_paragraph(style="List Bullet") + p.paragraph_format.left_indent = Inches(0.22) + p.paragraph_format.first_line_indent = Inches(-0.12) + p.paragraph_format.space_after = Pt(3) + run = p.add_run(text) + run.font.name = "Arial" + run.font.size = Pt(9.5) + return p + + +def build_docx(site_root, output_path): + index_path = site_root / "index.html" + tree = lxml_html.fromstring(index_path.read_text(encoding="utf-8")) + + name = clean(tree.xpath("string(//h1)")) + role = clean(tree.xpath("string(//*[contains(@class, 'role')])")) + availability = clean(tree.xpath("string(//*[contains(@class, 'availability')])")) + strengths = [clean(el.text_content()) for el in tree.xpath(".//*[contains(concat(' ', normalize-space(@class), ' '), ' strength-cloud ')]//span")] + + sections = {} + for section in tree.xpath(".//section[contains(concat(' ', normalize-space(@class), ' '), ' section ')]"): + title = clean(section.xpath("string(.//*[contains(@class, 'section-title')])")) + sections[title] = section + + doc = Document() + section = doc.sections[0] + section.top_margin = Inches(0.55) + section.bottom_margin = Inches(0.55) + section.left_margin = Inches(0.7) + section.right_margin = Inches(0.7) + + styles = doc.styles + styles["Normal"].font.name = "Arial" + styles["Normal"].font.size = Pt(9.5) + + title = doc.add_paragraph() + title.alignment = WD_ALIGN_PARAGRAPH.CENTER + title.paragraph_format.space_after = Pt(2) + r = title.add_run(name.upper()) + set_run(r, size=18, bold=True, color="111827") + + subtitle = doc.add_paragraph() + subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER + subtitle.paragraph_format.space_after = Pt(4) + r = subtitle.add_run(role) + set_run(r, size=10.5, bold=True, color="b91c1c") + + avail = doc.add_paragraph() + avail.alignment = WD_ALIGN_PARAGRAPH.CENTER + avail.paragraph_format.space_after = Pt(6) + r = avail.add_run(availability) + set_run(r, size=8.8, color="4b5563") + + contact_line = " | ".join(f"{label}: {value}" for label, value in CONTACTS.items()) + contact = doc.add_paragraph() + contact.alignment = WD_ALIGN_PARAGRAPH.CENTER + contact.paragraph_format.space_after = Pt(8) + r = contact.add_run(contact_line) + set_run(r, size=7.8, color="374151") + add_bottom_border(contact, "b91c1c", "10") + + profile_section = sections.get("Professional Profile") + if profile_section is not None: + add_heading(doc, "Professional Profile") + p = doc.add_paragraph() + p.paragraph_format.space_after = Pt(5) + r = p.add_run(clean(profile_section.xpath("string(.//*[contains(@class, 'profile-text')])"))) + set_run(r, size=9.5, color="111827") + + if strengths: + p = doc.add_paragraph() + p.paragraph_format.space_after = Pt(2) + r = p.add_run("Core strengths: ") + set_run(r, size=9, bold=True, color="111827") + r = p.add_run(", ".join(strengths)) + set_run(r, size=9, color="374151") + + tools_section = sections.get("Core Tools & Work Types") + if tools_section is not None: + add_heading(doc, "Core Tools & Work Types") + p = doc.add_paragraph() + p.paragraph_format.space_after = Pt(3) + r = p.add_run(clean(tools_section.xpath("string(.//*[contains(@class, 'profile-text')])"))) + set_run(r, size=9.2, color="111827") + + experience_section = sections.get("Professional Experience") + if experience_section is not None: + add_heading(doc, "Professional Experience") + for job in by_class(experience_section, "job-item"): + head = doc.add_paragraph() + head.paragraph_format.space_before = Pt(4) + head.paragraph_format.space_after = Pt(1) + title_text = clean(job.xpath("string(.//*[contains(@class, 'job-title')])")) + date_text = clean(job.xpath("string(.//*[contains(@class, 'job-meta')])")) + r = head.add_run(title_text) + set_run(r, size=10.2, bold=True, color="111827") + r = head.add_run(f" | {date_text}") + set_run(r, size=9, bold=True, color="b91c1c") + + sub = doc.add_paragraph() + sub.paragraph_format.space_after = Pt(2) + r = sub.add_run(clean(job.xpath("string(.//*[contains(@class, 'job-sub')])"))) + set_run(r, size=8.8, bold=True, color="6b7280") + + for li in by_class(job, "job-desc"): + for item in li.xpath(".//li"): + add_bullet(doc, clean(item.text_content())) + + education_section = sections.get("Education & Background") + if education_section is not None: + add_heading(doc, "Education & Background") + p = doc.add_paragraph() + p.paragraph_format.space_after = Pt(1) + r = p.add_run(clean(education_section.xpath("string(.//*[contains(@class, 'edu-title')])"))) + set_run(r, size=9.8, bold=True, color="111827") + p = doc.add_paragraph() + p.paragraph_format.space_after = Pt(4) + r = p.add_run(clean(education_section.xpath("string(.//*[contains(@class, 'edu-sub')])"))) + set_run(r, size=8.8, color="6b7280") + for paragraph in by_class(education_section, "edu-text"): + p = doc.add_paragraph() + p.paragraph_format.space_after = Pt(3) + r = p.add_run(clean(paragraph.text_content())) + set_run(r, size=9.2, color="111827") + + target_section = sections.get("Career Target") + if target_section is not None: + add_heading(doc, "Career Target") + p = doc.add_paragraph() + p.paragraph_format.space_after = Pt(0) + r = p.add_run(clean(target_section.xpath("string(.//*[contains(@class, 'profile-text')])"))) + set_run(r, size=9.5, color="111827") + + footer = section.footer.paragraphs[0] + footer.alignment = WD_ALIGN_PARAGRAPH.CENTER + r = footer.add_run("Jason Eugene Garrison | Resume | Last updated June 15, 2026") + set_run(r, size=7.5, color="6b7280") + + doc.save(output_path) + + +if __name__ == "__main__": + root = Path(sys.argv[1]).resolve() + out = Path(sys.argv[2]).resolve() + build_docx(root, out) diff --git a/scripts/publish-permissions.ps1 b/scripts/publish-permissions.ps1 new file mode 100644 index 0000000..8ffa671 --- /dev/null +++ b/scripts/publish-permissions.ps1 @@ -0,0 +1,39 @@ +$ErrorActionPreference = "Stop" + +$siteRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).ProviderPath + +$publicFiles = @( + "index.html", + "site.css", + "site.js", + "Resume_Jason_Eugene_Garrison.pdf", + "Resume_Jason_Eugene_Garrison.docx" +) + +foreach ($file in $publicFiles) { + $path = Join-Path $siteRoot $file + if (Test-Path -LiteralPath $path) { + try { + icacls $path /grant "*S-1-1-0:RX" | Out-Null + } catch { + Write-Warning "Could not update public permissions for ${file}: $($_.Exception.Message)" + } + } +} + +foreach ($privatePath in @(".old", "scripts", "README.md", ".htaccess")) { + $path = Join-Path $siteRoot $privatePath + if (Test-Path -LiteralPath $path) { + try { + if ($privatePath -eq ".old") { + icacls $path /remove:g "*S-1-1-0" | Out-Null + } else { + icacls $path /remove:g "*S-1-1-0" /T | Out-Null + } + } catch { + Write-Warning "Could not update private permissions for ${privatePath}: $($_.Exception.Message)" + } + } +} + +Write-Host "Published file permissions updated." diff --git a/scripts/save-old.ps1 b/scripts/save-old.ps1 new file mode 100644 index 0000000..a0ac751 --- /dev/null +++ b/scripts/save-old.ps1 @@ -0,0 +1,33 @@ +param( + [string]$Label = "snapshot" +) + +$ErrorActionPreference = "Stop" + +$siteRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).ProviderPath +$oldRoot = Join-Path $siteRoot ".old" +$safeLabel = ($Label -replace "[^A-Za-z0-9._-]", "-").Trim("-") + +if ([string]::IsNullOrWhiteSpace($safeLabel)) { + $safeLabel = "snapshot" +} + +$timestamp = Get-Date -Format "yyyyMMdd-HHmmss" +$snapshot = Join-Path $oldRoot "$timestamp-$safeLabel" + +New-Item -ItemType Directory -Force -Path $snapshot | Out-Null + +$oldDir = Get-Item -LiteralPath $oldRoot -Force +$oldDir.Attributes = $oldDir.Attributes -bor [System.IO.FileAttributes]::Hidden + +$excludeDirs = @(".git", ".old") + +Get-ChildItem -LiteralPath $siteRoot -Force | Where-Object { + $excludeDirs -notcontains $_.Name +} | ForEach-Object { + Copy-Item -LiteralPath $_.FullName -Destination $snapshot -Recurse -Force +} + +icacls (Resolve-Path -LiteralPath $snapshot).ProviderPath /remove:g "*S-1-1-0" /T | Out-Null + +Write-Host "Saved snapshot: $snapshot" diff --git a/site.css b/site.css new file mode 100644 index 0000000..7779b56 --- /dev/null +++ b/site.css @@ -0,0 +1,643 @@ +:root { + --primary: #b91c1c; + --text-main: #374151; + --text-muted: #6b7280; + --bg-body: #f3f4f6; + --bg-paper: #ffffff; + --bg-offset: #f9fafb; + --bg-title: #f7f8fa; + --border: #e5e7eb; + --shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); +} + +[data-theme="dark"] { + --primary: #ef4444; + --text-main: #e5e7eb; + --text-muted: #9ca3af; + --bg-body: #111827; + --bg-paper: #1f2937; + --bg-offset: #374151; + --bg-title: #263241; + --border: #4b5563; + --shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5); +} + +* { + box-sizing: border-box; +} + +body { + min-height: 100vh; + margin: 0; + padding: 40px 20px; + color: var(--text-main); + background: var(--bg-body); + font-family: "Roboto", Arial, sans-serif; + line-height: 1.6; + transition: background-color 0.3s, color 0.3s; +} + +a { + color: inherit; + text-decoration: none; + transition: color 0.2s; +} + +a:hover, +a:focus-visible { + color: var(--primary); +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +.resume-paper { + max-width: 900px; + margin: 0 auto; + overflow: hidden; + background: var(--bg-paper); + border-top: 8px solid var(--primary); + box-shadow: var(--shadow); + transition: background-color 0.3s; +} + +.action-bar { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 15px; + padding: 15px 40px; + background: var(--bg-offset); + border-bottom: 1px solid var(--border); +} + +.btn-dl { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 38px; + padding: 8px 16px; + color: var(--text-main); + background: var(--bg-paper); + border: 1px solid var(--border); + border-radius: 4px; + font: 700 0.85rem/1 "Roboto", Arial, sans-serif; + text-transform: uppercase; + cursor: pointer; +} + +.btn-dl:hover, +.btn-dl:focus-visible { + color: var(--primary); + border-color: var(--primary); + outline: none; +} + +.icon-btn { + min-width: 40px; + justify-content: center; + padding: 8px; +} + +.resume-header { + display: flex; + align-items: center; + gap: 40px; + padding: 50px 60px 40px; + border-bottom: 2px solid var(--border); +} + +.profile-img { + width: 150px; + height: 150px; + flex: 0 0 auto; + object-fit: cover; + padding: 4px; + background: var(--bg-paper); + border: 1px solid var(--border); + border-radius: 4px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.header-content h1 { + margin: 0 0 5px; + color: var(--text-main); + font-family: "Oswald", Arial, sans-serif; + font-size: 3rem; + line-height: 1.1; + text-transform: uppercase; +} + +.role { + margin: 0 0 20px; + color: var(--primary); + font-size: 1.1rem; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; +} + +.availability { + margin: -8px 0 20px; + color: var(--text-muted); + font-size: 0.95rem; + font-weight: 500; +} + +.contact-grid { + display: flex; + flex-wrap: wrap; + gap: 15px 25px; + margin: 0; + color: var(--text-main); + font-size: 0.9rem; + font-style: normal; + font-weight: 500; +} + +.contact-item { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.contact-item i { + color: var(--primary); + font-size: 1rem; +} + +.secure-link { + padding: 0; + color: inherit; + background: none; + border: 0; + border-bottom: 1px dotted var(--text-muted); + font: inherit; + cursor: pointer; +} + +.secure-link:hover, +.secure-link:focus-visible { + color: var(--primary); + border-bottom-color: var(--primary); + outline: none; +} + +.content-body { + padding: 50px 60px; +} + +.section { + margin-bottom: 50px; +} + +.compact-section { + margin-bottom: 42px; +} + +.final-section { + margin-bottom: 0; +} + +.section-title { + display: flex; + align-items: center; + margin: 0 0 28px; + padding: 10px 16px; + color: var(--text-main); + background: linear-gradient(90deg, var(--bg-title), transparent); + border-left: 5px solid var(--primary); + border-radius: 4px; + font-family: "Oswald", Arial, sans-serif; + font-size: 1.5rem; + text-transform: uppercase; +} + +.profile-text { + margin: 0 0 15px; + padding-left: 20px; + color: var(--text-main); + font-size: 1.1rem; +} + +.strength-cloud { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 5px 7px; + margin: 18px 0 2px 20px; + padding-top: 10px; + border-top: 1px solid var(--border); +} + +.strength-cloud span { + display: inline-flex; + align-items: center; + min-height: 0; + padding: 2px 7px; + color: var(--text-main); + background: color-mix(in srgb, var(--primary) 7%, var(--bg-paper)); + border: 1px solid color-mix(in srgb, var(--primary) 28%, var(--border)); + border-radius: 999px; + font-size: 0.72rem; + font-weight: 700; + line-height: 1.15; +} + +.strength-cloud .weight-2 { + min-height: 0; + padding: 2px 8px; + font-size: 0.75rem; +} + +.strength-cloud .weight-3 { + min-height: 0; + padding: 3px 9px; + color: #ffffff; + background: var(--primary); + border-color: var(--primary); + font-size: 0.8rem; +} + +.job-item { + position: relative; + margin-bottom: 35px; + padding-left: 28px; +} + +.job-item::before { + position: absolute; + top: 8px; + bottom: -26px; + left: 6px; + width: 1px; + background: var(--border); + content: ""; +} + +.job-item::after { + position: absolute; + top: 8px; + left: 2px; + width: 9px; + height: 9px; + background: var(--primary); + border: 2px solid var(--bg-paper); + border-radius: 50%; + box-shadow: 0 0 0 1px var(--primary); + content: ""; +} + +.job-item:last-child::before { + display: none; +} + +.job-head { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; + margin-bottom: 5px; +} + +.job-title { + margin: 0; + color: var(--text-main); + font-size: 1.25rem; + font-weight: 700; +} + +.job-meta { + margin: 0; + color: var(--primary); + font-family: "Oswald", Arial, sans-serif; + font-size: 0.95rem; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.job-sub { + margin: 0 0 10px; + color: var(--text-muted); + font-size: 0.9rem; + font-weight: 600; +} + +.job-desc { + margin: 0; + padding: 0; + list-style: none; +} + +.job-desc li { + position: relative; + margin-bottom: 8px; + padding-left: 20px; + color: var(--text-main); + font-size: 1rem; +} + +.job-desc li::before { + position: absolute; + left: 0; + color: var(--primary); + content: "\2022"; + font-size: 1.2rem; + font-weight: 700; + line-height: 1.4; +} + +.edu-box { + margin-left: 20px; + padding: 30px; + background: var(--bg-offset); + border: 1px solid var(--border); + border-radius: 4px; +} + +.edu-title { + margin: 0 0 5px; + color: var(--text-main); + font-size: 1.1rem; + font-weight: 700; +} + +.edu-sub { + margin: 0 0 15px; + color: var(--text-muted); + font-size: 0.9rem; + font-style: italic; +} + +.edu-text { + margin: 0 0 15px; + color: var(--text-main); +} + +.edu-text:last-child { + margin-bottom: 0; +} + +footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px 18px; + padding: 30px; + color: #9ca3af; + background: #111827; + font-size: 0.85rem; + letter-spacing: 1px; + text-align: center; + text-transform: uppercase; +} + +@media (max-width: 800px) { + body { + padding: 0; + } + + .resume-paper { + margin: 0; + border-top: 0; + box-shadow: none; + } + + .action-bar { + flex-wrap: wrap; + justify-content: center; + padding: 15px 20px; + } + + .resume-header { + flex-direction: column; + gap: 24px; + padding: 40px 20px; + text-align: center; + } + + .header-content h1 { + font-size: 2.25rem; + } + + .contact-grid { + justify-content: center; + } + + .content-body { + padding: 40px 20px; + } + + .job-head { + flex-direction: column; + gap: 5px; + } + + .profile-text, + .job-item, + .edu-box, + .strength-cloud { + margin-left: 0; + padding-left: 0; + } + + .job-item { + padding-left: 22px; + } + + .job-item::before { + left: 3px; + } + + .job-item::after { + left: -1px; + } +} + +@media print { + @page { + size: letter; + margin: 0.45in; + } + + :root { + --primary: #b91c1c; + --text-main: #000000; + --text-muted: #444444; + --bg-paper: #ffffff; + --bg-offset: #ffffff; + --border: #cccccc; + } + + body { + padding: 0; + color: black; + background: white; + font-size: 9pt; + line-height: 1.35; + } + + .resume-paper { + max-width: 100%; + overflow: visible; + border: 0; + border-top: 5px solid #b91c1c; + box-shadow: none; + } + + .action-bar { + display: none; + } + + .resume-header, + .content-body { + padding: 0; + } + + .resume-header { + gap: 18px; + padding-bottom: 12px; + } + + .profile-img { + width: 88px; + height: 88px; + } + + .header-content h1 { + margin-bottom: 2px; + font-size: 24pt; + } + + .role { + margin-bottom: 6px; + font-size: 10pt; + } + + .availability { + margin-bottom: 7px; + color: #444444; + font-size: 8.4pt; + } + + .contact-grid { + gap: 4px 14px; + font-size: 7.8pt; + } + + .content-body { + padding-top: 12px; + } + + .section, + .compact-section { + margin-bottom: 14px; + } + + .section-title { + margin-bottom: 7px; + padding: 4px 8px; + background: #f7f7f7; + border-left-color: black; + border-radius: 0; + font-size: 10.5pt; + } + + .profile-text { + margin-bottom: 6px; + padding-left: 10px; + font-size: 9pt; + } + + .strength-cloud { + gap: 3px 5px; + margin: 8px 0 0 10px; + padding-top: 6px; + } + + .strength-cloud span, + .strength-cloud .weight-2, + .strength-cloud .weight-3 { + padding: 1px 5px; + color: black; + background: white; + border-color: #cccccc; + font-size: 7.5pt; + } + + .job-item { + margin-bottom: 10px; + padding-left: 18px; + break-inside: avoid; + } + + .job-item::before { + bottom: -8px; + left: 3px; + } + + .job-item::after { + left: -1px; + background: #b91c1c; + border-color: white; + } + + .job-head { + margin-bottom: 1px; + } + + .job-title { + font-size: 9.7pt; + } + + .job-meta, + .job-sub { + font-size: 8.2pt; + } + + .job-desc li { + margin-bottom: 2px; + padding-left: 14px; + font-size: 8.7pt; + } + + .job-desc li::before { + font-size: 9pt; + line-height: 1.25; + } + + .edu-box { + margin-left: 10px; + padding: 10px; + background: white; + } + + .edu-title { + font-size: 9.2pt; + } + + .edu-sub, + .edu-text { + font-size: 8.7pt; + } + + .strength-cloud { + break-inside: avoid; + } + + a { + color: black; + text-decoration: none; + } + + .job-meta { + color: black; + } + + .secure-link { + color: black; + border: 0; + } +} diff --git a/site.js b/site.js new file mode 100644 index 0000000..5efe813 --- /dev/null +++ b/site.js @@ -0,0 +1,104 @@ +(function () { + const root = document.documentElement; + const toggleButton = document.getElementById("theme-toggle"); + const toggleIcon = toggleButton ? toggleButton.querySelector("i") : null; + + const storage = { + get(key) { + try { + return window.localStorage.getItem(key); + } catch (error) { + return null; + } + }, + set(key, value) { + try { + window.localStorage.setItem(key, value); + } catch (error) { + return false; + } + + return true; + } + }; + + const setTheme = (theme) => { + const isDark = theme === "dark"; + + if (isDark) { + root.setAttribute("data-theme", "dark"); + } else { + root.removeAttribute("data-theme"); + } + + if (toggleIcon) { + toggleIcon.classList.toggle("fa-sun", isDark); + toggleIcon.classList.toggle("fa-moon", !isDark); + } + + if (toggleButton) { + toggleButton.setAttribute("aria-pressed", String(isDark)); + toggleButton.title = isDark ? "Switch to light theme" : "Switch to dark theme"; + } + }; + + const savedTheme = storage.get("theme") || "light"; + setTheme(savedTheme); + + if (toggleButton) { + toggleButton.addEventListener("click", () => { + const nextTheme = root.hasAttribute("data-theme") ? "light" : "dark"; + setTheme(nextTheme); + storage.set("theme", nextTheme); + }); + } + + const printButton = document.getElementById("print-resume"); + if (printButton) { + printButton.addEventListener("click", () => window.print()); + } + + const contactKey = 43; + const contactValues = { + email: [65, 74, 88, 68, 69, 78, 94, 76, 78, 69, 78, 76, 74, 89, 89, 66, 88, 68, 69, 107, 66, 72, 71, 68, 94, 79, 5, 72, 68, 70], + ky: [3, 25, 28, 27, 2, 11, 19, 19, 18, 6, 19, 27, 28, 25], + tn: [3, 19, 29, 30, 2, 11, 18, 19, 30, 6, 26, 29, 31, 26] + }; + + const revealContact = (contactType) => { + const encoded = contactValues[contactType]; + + if (!encoded) { + return ""; + } + + return encoded.map((code) => String.fromCharCode(code ^ contactKey)).join(""); + }; + + const revealButtonContact = (button) => { + const contactType = button.dataset.contact; + const value = revealContact(contactType); + + if (!value) { + return; + } + + const href = contactType === "email" + ? `mailto:${value}` + : `tel:${value.replace(/\D/g, "")}`; + + const link = document.createElement("a"); + link.href = href; + link.rel = "nofollow"; + link.textContent = value; + button.replaceWith(link); + }; + + document.querySelectorAll("[data-contact]").forEach((button) => { + button.addEventListener("click", () => revealButtonContact(button)); + }); + + window.addEventListener("beforeprint", () => { + document.querySelectorAll("[data-contact]").forEach(revealButtonContact); + }); +})();