<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ed Cho — Technology Consultant | Cloud, Infrastructure &amp; Security</title>
<meta name="description" content="Chicago technology consultant — 15+ years in infrastructure, cloud, security, identity and IT leadership (Cloudflare, AWS, Okta). Systems that just work.">
<meta name="author" content="Ed Cho">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
<link rel="canonical" href="https://edcho.xyz/">
<link rel="preload" as="image" href="images/headshot.webp" fetchpriority="high">
<meta name="theme-color" content="#faf7f0">
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="favicon.svg">
<!-- Open Graph / social -->
<meta property="og:type" content="website">
<meta property="og:site_name" content="Ed Cho">
<meta property="og:locale" content="en_US">
<meta property="og:title" content="Ed Cho — Technology Consultant">
<meta property="og:description" content="Technology that serves people — not the other way around. Infrastructure, cloud, security &amp; fractional tech leadership.">
<meta property="og:url" content="https://edcho.xyz/">
<meta property="og:image" content="https://edcho.xyz/images/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Ed Cho — technology consulting">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Ed Cho — Technology Consultant">
<meta name="twitter:description" content="Infrastructure, cloud, security &amp; fractional tech leadership for organizations that want systems that just work.">
<meta name="twitter:image" content="https://edcho.xyz/images/og-image.png">
<!-- Self-hosted fonts are declared inside styles.css (woff2 files in /fonts/) -->
<style>
/* Ed Cho — technology consulting. Warm editorial / engineering aesthetic. */

/* Self-hosted fonts (SIL OFL — see fonts/LICENSE-*.txt). Loaded here so there is
   no separate fonts.css request that could 404. woff2 files live in /fonts/. */
@font-face{font-family:"Fraunces";font-style:normal;font-weight:400;font-display:swap;src:url("fonts/fraunces-latin-400-normal.woff2") format("woff2")}
@font-face{font-family:"Fraunces";font-style:normal;font-weight:500;font-display:swap;src:url("fonts/fraunces-latin-500-normal.woff2") format("woff2")}
@font-face{font-family:"Fraunces";font-style:normal;font-weight:600;font-display:swap;src:url("fonts/fraunces-latin-600-normal.woff2") format("woff2")}
@font-face{font-family:"Hanken Grotesk";font-style:normal;font-weight:400;font-display:swap;src:url("fonts/hanken-grotesk-latin-400-normal.woff2") format("woff2")}
@font-face{font-family:"Hanken Grotesk";font-style:normal;font-weight:500;font-display:swap;src:url("fonts/hanken-grotesk-latin-500-normal.woff2") format("woff2")}
@font-face{font-family:"Hanken Grotesk";font-style:normal;font-weight:600;font-display:swap;src:url("fonts/hanken-grotesk-latin-600-normal.woff2") format("woff2")}
@font-face{font-family:"JetBrains Mono";font-style:normal;font-weight:400;font-display:swap;src:url("fonts/jetbrains-mono-latin-400-normal.woff2") format("woff2")}
@font-face{font-family:"JetBrains Mono";font-style:normal;font-weight:700;font-display:swap;src:url("fonts/jetbrains-mono-latin-700-normal.woff2") format("woff2")}

:root{
  --paper:#faf7f0;          /* warm cream */
  --paper-2:#f3eee2;        /* deeper sand surface */
  --card:#fffdf8;           /* card */
  --ink:#221d16;            /* warm near-black */
  --ink-2:#5b5345;          /* muted warm */
  --ink-3:#6e6657;          /* faint, but AA-compliant on paper */
  --line:#e4ddcc;           /* hairline */
  --teal:#0f8e6c;           /* primary accent */
  --teal-d:#0a6e54;         /* teal for text on light / button bg (AA on white) */
  --teal-dd:#075a44;        /* darker teal for button hover */
  --teal-2:#e2f1ea;         /* teal tint surface */
  --coral:#d65a2c;          /* warm accent */
  --coral-d:#b3441b;
  --coral-2:#fbe9df;        /* coral tint surface */
  --radius:14px;
  --radius-lg:22px;
  --maxw:1120px;
  --shadow:0 1px 2px rgba(34,29,22,.04), 0 8px 28px -12px rgba(34,29,22,.14);
  --shadow-lift:0 2px 4px rgba(34,29,22,.06), 0 20px 44px -16px rgba(34,29,22,.22);
  /* night register — pulled from the terminal / favicon family */
  --panel:#1b1712; --panel-2:#241f17; --panel-line:#373025;
  --phos:#4ad6a9; --amber:#e2a35c; --red:#e0685c; --dim:#9b9384;
  color-scheme:light;
  --font-display:"Fraunces", Georgia, "Times New Roman", serif;
  --font-body:"Hanken Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono:"JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
}
*{box-sizing:border-box}
html,body{overflow-x:clip}
.hero-grid>*,.strip-in>*{min-width:0}
html{scroll-behavior:smooth; -webkit-text-size-adjust:100%}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
body{
  margin:0; background:var(--paper); color:var(--ink);
  font-family:var(--font-body); font-size:17px; line-height:1.7; font-weight:400;
  -webkit-font-smoothing:antialiased; letter-spacing:-.005em;
  background-image:radial-gradient(rgba(34,29,22,.035) 1px, transparent 1px);
  background-size:22px 22px;
}
img{max-width:100%; display:block}
::selection{background:var(--teal-2); color:var(--ink)}
:focus-visible{outline:2px solid var(--teal-d); outline-offset:3px; border-radius:6px}
a{color:var(--teal-d); text-decoration:none}
a:hover{color:var(--coral-d)}
h1,h2{text-wrap:balance}
h1,h2,h3{font-family:var(--font-display); font-weight:500; line-height:1.08; letter-spacing:-.02em; margin:0; overflow-wrap:break-word}
h2{font-size:clamp(1.9rem,4vw,2.9rem)}
h3{font-size:1.3rem; line-height:1.2}
.wrap{max-width:var(--maxw); margin:0 auto; padding:0 24px}
.mono{font-family:var(--font-mono); font-size:.8rem; letter-spacing:.02em}
.eyebrow{font-family:var(--font-mono); font-size:.8rem; color:var(--coral-d); letter-spacing:.04em; text-transform:lowercase}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}

/* ---- nav ---- */
header.nav{position:sticky; top:0; z-index:50; background:rgba(250,247,240,.78);
  -webkit-backdrop-filter:blur(14px) saturate(1.5); backdrop-filter:blur(14px) saturate(1.5); border-bottom:1px solid var(--line)}
.nav-in{display:flex; align-items:center; justify-content:space-between; height:66px;
  max-width:var(--maxw); margin:0 auto; padding:0 24px}
.brand{display:flex; align-items:center; gap:9px; font-weight:500}
.brand .mark{display:inline-flex; align-items:center; gap:3px; font-family:var(--font-mono);
  font-weight:700; color:var(--ink); font-size:1.12rem; letter-spacing:-.01em}
.brand .mark .cur{display:inline-block; width:7px; height:1.05em; background:var(--teal);
  border-radius:1px; animation:blink 1.15s steps(1,end) infinite}
@keyframes blink{50%{opacity:0}}
@media (prefers-reduced-motion:reduce){.brand .mark .cur{animation:none}}
.brand b{font-family:var(--font-display); font-weight:500; font-size:1.12rem; letter-spacing:-.01em}
.nav-links{display:flex; align-items:center; gap:26px}
.nav-links a{color:var(--ink-2); font-size:.95rem}
.nav-links a:not(.btn){background:linear-gradient(currentColor,currentColor) no-repeat 0 100%/0 1px;
  padding-bottom:2px; transition:background-size .25s ease, color .2s ease}
.nav-links a:not(.btn):hover{background-size:100% 1px}
.nav-links a:hover{color:var(--ink)}
.nav-links a.btn-primary,.nav-links a.btn-primary:hover{color:#fff}
.btn{display:inline-flex; align-items:center; gap:8px; font-family:var(--font-body); font-weight:500;
  font-size:.95rem; padding:11px 18px; border-radius:11px; cursor:pointer; border:1.5px solid transparent;
  transition:transform .15s ease, box-shadow .2s ease, background .2s ease}
.btn:active{transform:scale(.98)}
.btn-primary{background:var(--teal-d); color:#fff; box-shadow:0 6px 16px -8px rgba(10,110,84,.85)}
.btn-primary:hover{background:var(--teal-dd); color:#fff; box-shadow:0 10px 22px -8px rgba(7,90,68,.9); transform:translateY(-1px)}
.btn-ghost{border-color:var(--line); color:var(--ink); background:transparent}
.btn-ghost:hover{border-color:var(--ink-3); color:var(--ink); background:var(--card); transform:translateY(-1px)}
.nav-toggle{display:none; background:none; border:0; cursor:pointer; padding:8px}
.nav-toggle span{display:block; width:22px; height:2px; background:var(--ink); margin:4px 0; border-radius:2px; transition:.25s}

/* ---- hero ---- */
.hero{position:relative; overflow:hidden}
.hero::before{content:""; position:absolute; inset:0; pointer-events:none;
  background:radial-gradient(640px 420px at 16% 22%, rgba(15,142,108,.08), transparent 62%),
             radial-gradient(560px 360px at 86% 8%, rgba(214,90,44,.065), transparent 60%)}
.hero .wrap{position:relative}
.hero-grid{display:grid; grid-template-columns:1.15fr .85fr; gap:54px; align-items:center;
  padding:clamp(48px,8vw,104px) 0 clamp(40px,6vw,76px)}
.hero h1{font-size:clamp(2.6rem,6vw,4.6rem); margin:18px 0 0; letter-spacing:-.03em}
.hero h1 em{font-style:normal; color:var(--teal-d); position:relative}
.hero h1 em::after{content:""; position:absolute; left:0; right:0; bottom:.06em; height:.14em;
  background:var(--coral-2); z-index:-1; border-radius:3px; transform:scaleX(0); transform-origin:left;
  animation:sweep .65s .55s cubic-bezier(.2,.7,.2,1) forwards}
@keyframes sweep{to{transform:scaleX(1)}}
@media (prefers-reduced-motion:reduce){.hero h1 em::after{animation:none; transform:none}}
.hero p.lead{font-size:1.18rem; color:var(--ink-2); max-width:33ch; margin:22px 0 30px}
.hero-cta{display:flex; gap:12px; flex-wrap:wrap}
.avail{display:inline-flex; align-items:center; gap:8px; margin-top:26px; font-size:.9rem; color:var(--ink-2)}
.dot{position:relative; width:9px; height:9px; border-radius:50%; background:var(--teal); box-shadow:0 0 0 4px var(--teal-2)}
.dot::after{content:""; position:absolute; inset:-3px; border-radius:50%; border:2px solid var(--teal);
  opacity:0; animation:ping 2.4s ease-out infinite}
@keyframes ping{0%{transform:scale(.55); opacity:.65} 75%,100%{transform:scale(1.7); opacity:0}}
@media (prefers-reduced-motion:reduce){.dot::after{animation:none}}
.hero-photo{position:relative; justify-self:center}
.hero-photo .frame{position:relative; width:min(360px,78vw); aspect-ratio:1; border-radius:26px; overflow:hidden;
  box-shadow:var(--shadow-lift); border:5px solid var(--card); outline:1px solid var(--line); outline-offset:7px; z-index:2; rotate:1.5deg}
.hero-photo .frame img{width:100%; height:100%; object-fit:cover}
.hero-photo .blob{position:absolute; inset:-18px -18px auto auto; width:62%; aspect-ratio:1;
  background:var(--teal-2); border-radius:30px; rotate:-8deg; z-index:1}
.hero-photo .chip{position:absolute; z-index:3; background:var(--card); border:1px solid var(--line);
  border-radius:12px; padding:9px 13px; box-shadow:var(--shadow); font-family:var(--font-mono); font-size:.74rem; color:var(--ink-2)}
.hero-photo .chip b{color:var(--teal-d)}
.chip-1{left:-26px; top:34px; rotate:-4deg}
.chip-2{right:-22px; bottom:40px; rotate:5deg}

/* ---- stat strip ---- */
.strip{border-top:1px solid var(--line); border-bottom:1px solid var(--line); background:var(--paper-2)}
.strip-in{display:grid; grid-template-columns:repeat(3,1fr); gap:24px; padding:30px 0}
.strip-in .stat + .stat{border-left:1px solid var(--line); padding-left:24px}
@media (max-width:880px){.strip-in .stat + .stat{border-left:0; padding-left:0; border-top:1px dashed var(--line); padding-top:16px}}
.stat .n{font-family:var(--font-display); font-size:2rem; color:var(--ink); line-height:1}
.stat .l{font-size:.92rem; color:var(--ink-2); margin-top:4px}
.chips{display:flex; flex-wrap:wrap; gap:8px; margin-top:8px}
.chips.marquee{flex-wrap:nowrap; overflow:hidden;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent)}
.chips-track{display:flex; gap:8px; flex-shrink:0; min-width:max-content; padding-right:8px;
  animation:chipscroll 28s linear infinite}
.chips.marquee:hover .chips-track{animation-play-state:paused}
@keyframes chipscroll{to{transform:translateX(-100%)}}
@media (prefers-reduced-motion:reduce){
  .chips-track{animation:none; flex-wrap:wrap; min-width:0}
  .chips-track + .chips-track{display:none}
  .chips.marquee{-webkit-mask-image:none; mask-image:none; flex-wrap:wrap}
}
.tag{font-family:var(--font-mono); font-size:.72rem; color:var(--teal-d); background:var(--teal-2);
  padding:5px 10px; border-radius:7px}

/* ---- sections ---- */
section{padding:clamp(56px,9vw,104px) 0; scroll-margin-top:78px}
.sec-head{max-width:40ch; margin-bottom:42px}
.sec-head h2{margin-top:10px}
.sec-head p{color:var(--ink-2); margin:14px 0 0}

/* services */
.svc{display:grid; grid-template-columns:repeat(2,1fr); gap:18px}
.svc .card{background:var(--card); border:1px solid var(--line); border-radius:var(--radius-lg);
  padding:28px; transition:transform .2s ease, box-shadow .25s ease, border-color .25s ease}
.svc .card:hover{transform:translateY(-4px); box-shadow:var(--shadow-lift); border-color:#bcd8cc}
.svc .card:hover .ic{transform:scale(1.06) rotate(-3deg)}
.svc .ic{transition:transform .25s cubic-bezier(.2,.7,.2,1)}
.svc .ic{width:46px; height:46px; border-radius:12px; background:var(--teal-2); color:var(--teal-d);
  display:grid; place-items:center; margin-bottom:16px}
.svc .ic svg{width:24px; height:24px}
.svc h3{margin-bottom:8px}
.svc p{color:var(--ink-2); font-size:.98rem; margin:0}

/* approach */
.approach{background:var(--ink); color:#f3eee2; border-radius:28px; padding:clamp(34px,5vw,64px);
  position:relative; overflow:hidden}
.approach h2{color:#fff}
.approach .eyebrow{color:#f0a884}
.approach p.big{font-family:var(--font-display); font-size:clamp(1.4rem,3vw,2rem); line-height:1.3;
  color:#f6f1e6; max-width:24ch; margin:16px 0 0; font-weight:400}
.steps{display:grid; grid-template-columns:repeat(3,1fr); gap:22px; margin-top:46px}
.step .k{font-family:var(--font-mono); font-size:.78rem; color:#6fd3b3}
.step h3{color:#fff; margin:10px 0 6px; font-size:1.15rem}
.step p{color:#cbc2b0; font-size:.95rem; margin:0}

/* work */
.work-grid{display:grid; grid-template-columns:repeat(2,1fr); gap:22px}
.case{background:var(--card); border:1px solid var(--line); border-radius:var(--radius-lg); overflow:hidden;
  display:flex; flex-direction:column; transition:transform .2s ease, box-shadow .25s ease}
.case:hover{transform:translateY(-4px); box-shadow:var(--shadow-lift); border-color:#bcd8cc}
.case .shot{aspect-ratio:16/10; overflow:hidden; background:var(--paper-2); border-bottom:1px solid var(--line)}
.case .shot img{width:100%; height:100%; object-fit:cover; object-position:top}
.case .body{padding:22px 24px 24px}
.case h3{font-size:1.16rem}
.case .meta{font-family:var(--font-mono); font-size:.78rem; color:var(--ink-2); margin-top:12px; line-height:1.85}
.case .meta b{color:var(--teal-d); font-weight:400}
.case.text-only{justify-content:center; padding:8px}
.case.text-only .body{padding:30px 26px}
.case .lede{color:var(--ink-2); font-size:.98rem; margin:8px 0 0}

/* about */
.about-grid{display:grid; grid-template-columns:1fr 1fr; gap:52px; align-items:center}
.collage{display:grid; grid-template-columns:1fr 1fr; grid-auto-rows:130px; gap:12px}
.collage img{width:100%; height:100%; object-fit:cover; border-radius:14px; box-shadow:var(--shadow)}
.collage .tall{grid-row:span 2}
.about-copy h2{margin-bottom:6px}
.about-copy p{color:var(--ink-2)}
.creds{display:flex; flex-wrap:wrap; gap:10px; margin-top:22px}
.cred{font-family:var(--font-mono); font-size:.74rem; color:var(--ink-2); border:1px solid var(--line);
  border-radius:8px; padding:7px 11px; background:var(--card)}

/* contact */
.contact{background:var(--paper-2); border-top:1px solid var(--line)}
.contact-card{background:linear-gradient(180deg,var(--card),var(--paper)); border:1px solid var(--line);
  border-radius:28px; padding:clamp(36px,5vw,60px); text-align:center; box-shadow:var(--shadow)}
.contact-card h2{max-width:18ch; margin:8px auto 0}
.contact-card p{color:var(--ink-2); max-width:44ch; margin:16px auto 28px}
.contact-actions{display:flex; gap:12px; justify-content:center; flex-wrap:wrap}
.contact-meta{display:flex; gap:24px; justify-content:center; flex-wrap:wrap; margin-top:30px;
  font-family:var(--font-mono); font-size:.8rem; color:var(--ink-2)}
.contact-meta a{color:var(--ink-2)}
.contact-meta a:hover{color:var(--teal-d)}

/* footer */
footer.foot{padding:40px 0; border-top:1px solid var(--line)}
.foot-in{display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:16px}
.foot .mono{color:var(--ink-3)}
.foot-soc{display:flex; gap:16px}
.foot-soc a{color:var(--ink-2)}
.foot-soc a:hover{color:var(--teal-d)}
.foot-soc svg{width:20px; height:20px}

/* reveal animation */
.reveal{opacity:0; transform:translateY(18px); transition:opacity .7s cubic-bezier(.2,.7,.2,1), transform .7s cubic-bezier(.2,.7,.2,1)}
.reveal.in{opacity:1; transform:none}
@media (prefers-reduced-motion:reduce){.reveal{opacity:1; transform:none; transition:none}}

/* responsive */
@media (max-width:1040px){
  .wrap{padding-left:28px; padding-right:28px}
  .hero-grid{grid-template-columns:1fr; gap:38px; padding-top:24px}
  .hero h1{font-size:clamp(2.1rem,5vw,3.2rem); line-height:1.13}
  .hero-photo{order:-1}
}
@media (max-width:880px){
  .about-grid{grid-template-columns:1fr; gap:34px}
  .svc,.work-grid,.steps{grid-template-columns:1fr}
  .strip-in{grid-template-columns:1fr; gap:18px; text-align:left}
  .nav-links{position:fixed; inset:66px 0 auto 0; flex-direction:column; gap:0; background:var(--paper);
    border-bottom:1px solid var(--line); padding:8px 24px 18px; transform:translateY(-130%);
    transition:transform .3s ease; align-items:stretch}
  .nav-links.open{transform:none}
  .nav-links a{padding:13px 0; border-bottom:1px solid var(--line)}
  .nav-links .btn{margin-top:12px; justify-content:center}
  .nav-toggle{display:block}
}
@media (max-width:520px){
  .wrap{padding-left:28px; padding-right:28px}
  .collage{grid-auto-rows:108px}
  .hero h1{font-size:2.1rem; line-height:1.15}
  .hero p.lead{font-size:1.08rem}
}

/* grain texture for dark panels */
.grain::after{content:""; position:absolute; inset:0; pointer-events:none; opacity:.045;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); background-size:140px 140px}

/* ---- domain diagnostics panel ---- */
.diag{background:var(--panel); border-radius:28px; padding:clamp(30px,4.5vw,54px); color:#ece6da;
  position:relative; overflow:hidden; box-shadow:var(--shadow-lift)}
.diag::before{content:""; position:absolute; inset:0; pointer-events:none; opacity:.5;
  background:repeating-linear-gradient(0deg, transparent 0 3px, rgba(0,0,0,.14) 3px 4px)}
.diag>*{position:relative}
.diag .eyebrow{color:var(--phos)}
.diag h2{color:#f6f1e6; margin-top:10px}
.diag .sub{color:var(--dim); max-width:52ch; margin:14px 0 26px}
.diag-form{display:flex; gap:10px; flex-wrap:wrap; align-items:stretch}
.diag-form .field{flex:1; min-width:220px; display:flex; align-items:center; gap:9px;
  background:var(--panel-2); border:1px solid var(--panel-line); border-radius:12px; padding:0 14px;
  font-family:var(--font-mono); transition:border-color .2s ease}
.diag-form .field:focus-within{border-color:var(--phos)}
.diag-form .field .p{color:var(--phos); font-size:.85rem; user-select:none}
.diag-form input{flex:1; min-width:0; background:none; border:0; outline:0; color:#ece6da;
  font-family:var(--font-mono); font-size:.92rem; padding:13px 0; caret-color:var(--phos)}
.diag-form input::placeholder{color:#6d6557}
.diag-form .btn-run{background:var(--phos); color:#10231c; border:0; border-radius:12px; font-weight:600;
  font-family:var(--font-body); font-size:.95rem; padding:13px 22px; cursor:pointer;
  transition:transform .15s ease, filter .2s ease}
.diag-form .btn-run:hover{filter:brightness(1.08); transform:translateY(-1px)}
.diag-form .btn-run:disabled{opacity:.55; cursor:progress; transform:none}
.diag-note{font-family:var(--font-mono); font-size:.72rem; color:#6d6557; margin:12px 0 0}
.diag-out{margin-top:26px; display:none}
.diag-out.on{display:grid; grid-template-columns:auto 1fr; gap:26px; align-items:start}
.diag-grade{text-align:center; background:var(--panel-2); border:1px solid var(--panel-line);
  border-radius:16px; padding:20px 26px; min-width:118px}
.diag-grade .g{font-family:var(--font-display); font-weight:600; font-size:3.4rem; line-height:1; color:var(--phos)}
.diag-grade .g.warn{color:var(--amber)} .diag-grade .g.bad{color:var(--red)}
.diag-grade .s{font-family:var(--font-mono); font-size:.72rem; color:var(--dim); margin-top:6px}
.diag-rows{display:flex; flex-direction:column; gap:0; border:1px solid var(--panel-line); border-radius:14px;
  overflow:hidden; background:var(--panel-2)}
.drow{display:grid; grid-template-columns:15px 128px 1fr; gap:12px; align-items:baseline;
  padding:11px 16px; font-family:var(--font-mono); font-size:.82rem; border-bottom:1px solid var(--panel-line)}
.drow:last-child{border-bottom:0}
.drow .led{width:9px; height:9px; border-radius:50%; align-self:center; background:#4a4338}
.drow.pass .led{background:var(--phos); box-shadow:0 0 8px rgba(74,214,169,.7)}
.drow.warn .led{background:var(--amber); box-shadow:0 0 8px rgba(226,163,92,.6)}
.drow.fail .led{background:var(--red); box-shadow:0 0 8px rgba(224,104,92,.6)}
.drow .k{color:#cfc8ba}
.drow .v{color:var(--dim); word-break:break-word}
.drow.pass .v b{color:var(--phos); font-weight:400}
.drow.warn .v b{color:var(--amber); font-weight:400}
.drow.fail .v b{color:var(--red); font-weight:400}
.drow .fix{display:block; color:#7d7566; margin-top:2px}
.diag-cta{margin-top:18px; font-size:.95rem; color:var(--dim)}
.diag-cta a{color:var(--phos)}
.diag-cta a:hover{color:#7fe4c4}
.diag-scan{height:2px; margin-top:26px; border-radius:2px; overflow:hidden; background:var(--panel-2); display:none}
.diag-scan.on{display:block}
.diag-scan i{display:block; height:100%; width:34%; background:linear-gradient(90deg,transparent,var(--phos),transparent);
  animation:scan 1.1s linear infinite}
@keyframes scan{from{transform:translateX(-110%)} to{transform:translateX(400%)}}
@media (prefers-reduced-motion:reduce){.diag-scan i{animation:none; width:100%; opacity:.4}}
@media (max-width:720px){
  .diag-out.on{grid-template-columns:1fr}
  .diag-grade{justify-self:start; display:flex; align-items:center; gap:14px; text-align:left; padding:14px 18px}
  .diag-grade .g{font-size:2.4rem}
  .drow{grid-template-columns:15px 1fr; row-gap:2px}
  .drow .v{grid-column:2}
}

/* ---- interactive terminal ---- */
.term-open{display:inline-grid; place-items:center; font-family:var(--font-mono); font-weight:700;
  font-size:.95rem; color:var(--teal-d); background:var(--teal-2); border:0; border-radius:8px;
  width:40px; height:34px; cursor:pointer; transition:background .2s ease}
.term-open:hover{background:#cfe7dd}
.term{position:fixed; inset:0; z-index:100; display:flex; align-items:flex-start; justify-content:center;
  padding:8vh 16px 16px; background:rgba(20,16,12,.55); -webkit-backdrop-filter:blur(3px); backdrop-filter:blur(3px)}
.term[hidden]{display:none}
.term-box{width:100%; max-width:640px; background:#1c1812; color:#ece6da; border:1px solid #3a342a;
  border-radius:14px; overflow:hidden; box-shadow:0 30px 80px -20px rgba(0,0,0,.6);
  font-family:var(--font-mono); display:flex; flex-direction:column; max-height:84vh; animation:termin .18s ease}
@keyframes termin{from{opacity:0; transform:translateY(-10px) scale(.99)} to{opacity:1; transform:none}}
@media (prefers-reduced-motion:reduce){.term-box{animation:none}}
.term-bar{display:flex; align-items:center; gap:10px; padding:9px 12px; background:#16120d; border-bottom:1px solid #2c271f}
.term-dots{display:flex; gap:6px}
.term-dots i{width:11px; height:11px; border-radius:50%; background:#4a4338; display:block}
.term-dots i:first-child{background:#e0644a}
.term-title{font-size:.78rem; color:#9b9384; flex:1; text-align:center}
.term-x{font-family:var(--font-mono); font-size:.72rem; color:#9b9384; background:none;
  border:1px solid #3a342a; border-radius:6px; padding:3px 8px; cursor:pointer}
.term-x:hover{color:#ece6da; border-color:#5a5347}
.term-body{padding:14px 14px 6px; overflow-y:auto; font-size:.86rem; line-height:1.65; flex:1}
.term-body a{color:#5fd0aa; text-decoration:underline}
.term-line{white-space:pre-wrap; margin:0 0 2px; word-break:break-word}
.term-line .cmd{color:#ece6da}
.term-line .muted{color:#9b9384}
.term-line .teal{color:#5fd0aa}
.term-line .amber{color:#e8a06a}
.term-form{display:flex; align-items:center; gap:8px; padding:10px 14px 14px; border-top:1px solid #2c271f}
.term-prompt{color:#5fd0aa; font-size:.86rem; white-space:nowrap}
.term-form input{flex:1; min-width:0; background:none; border:0; outline:0; color:#ece6da;
  font-family:var(--font-mono); font-size:.86rem; caret-color:#5fd0aa}
@media (max-width:880px){.term-open{margin:12px 0}}

</style>
<script type="application/ld+json">
{
  "@context":"https://schema.org",
  "@graph":[
    {
      "@type":"Person",
      "@id":"https://edcho.xyz/#ed",
      "name":"Ed Cho",
      "jobTitle":"Technology Consultant",
      "url":"https://edcho.xyz/",
      "image":"https://edcho.xyz/images/headshot.webp",
      "email":"say@hello.edcho.xyz",
      "address":{"@type":"PostalAddress","addressLocality":"Chicago","addressRegion":"IL","addressCountry":"US"},
      "knowsAbout":["IT infrastructure","Cloudflare","Amazon Web Services","Okta","identity and access management","cybersecurity","data privacy","fractional CTO","technology leadership"],
      "sameAs":["https://www.linkedin.com/in/helloed","https://bsky.app/profile/edcho.xyz"]
    },
    {
      "@type":"ProfessionalService",
      "@id":"https://edcho.xyz/#service",
      "name":"Ed Cho — Technology Consulting",
      "url":"https://edcho.xyz/",
      "image":"https://edcho.xyz/images/og-image.png",
      "email":"say@hello.edcho.xyz",
      "founder":{"@id":"https://edcho.xyz/#ed"},
      "areaServed":["Chicago","United States","Remote"],
      "serviceType":["Cloud & infrastructure consulting","Security & identity (Okta, SSO)","Fractional technology leadership","Data privacy consulting"],
      "address":{"@type":"PostalAddress","addressLocality":"Chicago","addressRegion":"IL","addressCountry":"US"}
    },
    {
      "@type":"WebSite",
      "@id":"https://edcho.xyz/#website",
      "url":"https://edcho.xyz/",
      "name":"Ed Cho",
      "publisher":{"@id":"https://edcho.xyz/#ed"}
    }
  ]
}
</script>
</head>
<body>

<header class="nav">
  <div class="nav-in">
    <a class="brand" href="#top" aria-label="Ed Cho — home">
      <span class="mark">ec<span class="cur" aria-hidden="true"></span></span><b>Ed Cho</b>
    </a>
    <button class="nav-toggle" aria-label="Toggle menu" aria-expanded="false"><span></span><span></span><span></span></button>
    <nav class="nav-links" id="menu">
      <a href="#services">services</a>
      <a href="#approach">approach</a>
      <a href="#work">work</a>
      <a href="#check">domain check</a>
      <a href="#about">about</a>
      <button class="term-open" id="termOpen" type="button" aria-label="Open terminal (backtick key)" title="Open terminal  (  &#96;  )">&gt;_</button>
      <a class="btn btn-primary" href="#contact">Start a conversation</a>
    </nav>
  </div>
</header>

<main id="top">

  <!-- HERO -->
  <section class="hero">
    <div class="wrap hero-grid">
      <div class="hero-copy">
        <span class="eyebrow reveal">// technology consulting · chicago //</span>
        <h1 class="reveal">Technology that <em>serves people</em> — not the other way around.</h1>
        <p class="lead reveal">I help organizations turn fragile, frustrating systems into ones that quietly just work — so your team can get back to the work that matters.</p>
        <div class="hero-cta reveal">
          <a class="btn btn-primary" href="#contact">Let&rsquo;s talk &rarr;</a>
          <a class="btn btn-ghost" href="#work">See selected work</a>
        </div>
        <span class="avail reveal"><span class="dot"></span> Available for new consulting engagements</span>
      </div>
      <div class="hero-photo reveal">
        <span class="blob"></span>
        <div class="frame"><img src="images/headshot.webp" alt="Portrait of Ed Cho, technology consultant" width="720" height="720" fetchpriority="high"></div>
        <span class="chip chip-1">15+ yrs <b>in the trenches</b></span>
        <span class="chip chip-2"><b>infra</b> · cloud · security</span>
      </div>
    </div>
  </section>

  <!-- STAT STRIP -->
  <div class="strip">
    <div class="wrap strip-in">
      <div class="stat reveal"><div class="n">15+ years</div><div class="l">building &amp; leading technology teams</div></div>
      <div class="stat reveal"><div class="n">Global scale</div><div class="l">infrastructure, applications &amp; data</div></div>
      <div class="stat reveal">
        <div class="l" style="margin-bottom:2px">tools I reach for</div>
        <div class="chips marquee"><div class="chips-track"><span class="tag">aws</span><span class="tag">azure</span><span class="tag">cloudflare</span><span class="tag">jira</span><span class="tag">okta</span><span class="tag">next.js</span><span class="tag">php</span><span class="tag">salesforce</span><span class="tag">sql</span><span class="tag">wordpress</span><span class="tag">microsoft 365</span></div><div class="chips-track" aria-hidden="true"><span class="tag">aws</span><span class="tag">azure</span><span class="tag">cloudflare</span><span class="tag">jira</span><span class="tag">okta</span><span class="tag">next.js</span><span class="tag">php</span><span class="tag">salesforce</span><span class="tag">sql</span><span class="tag">wordpress</span><span class="tag">microsoft 365</span></div></div>
      </div>
    </div>
  </div>

  <!-- SERVICES -->
  <section id="services">
    <div class="wrap">
      <div class="sec-head reveal">
        <span class="eyebrow">// what i do //</span>
        <h2>Steady hands for the hard, unglamorous parts.</h2>
        <p>Whether you&rsquo;re a startup, a small business, or a global nonprofit, the goal is the same: technology you don&rsquo;t have to think about.</p>
      </div>
      <div class="svc">
        <div class="card reveal">
          <div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19a4.5 4.5 0 0 0 .5-8.97A6 6 0 0 0 6.5 9 4.5 4.5 0 0 0 7 19h10.5Z"/></svg></div>
          <h3>Cloud &amp; infrastructure</h3>
          <p>Edge, migrations, resilience and cost control on Cloudflare and AWS. Fewer 2 a.m. pages, lower bills, room to grow.</p>
        </div>
        <div class="card reveal">
          <div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l7 3v5c0 4.4-3 8-7 10-4-2-7-5.6-7-10V6l7-3Z"/><path d="M9.5 12l1.8 1.8L15 10"/></svg></div>
          <h3>Security &amp; identity</h3>
          <p>SSO, Okta, sane access controls and DDoS mitigation that actually holds up under real traffic and real attacks.</p>
        </div>
        <div class="card reveal">
          <div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3"/><path d="M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6"/><circle cx="17.5" cy="9" r="2.3"/><path d="M16 14.2c2.8.6 5 2.9 5 5.8"/></svg></div>
          <h3>Fractional tech leadership</h3>
          <p>A senior technology partner for strategy, vendors, budgets and teams — the value of a CTO without a full-time hire.</p>
        </div>
        <div class="card reveal">
          <div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/><circle cx="12" cy="15.5" r="1.3"/></svg></div>
          <h3>Privacy &amp; data</h3>
          <p>Practical, people-first privacy and data hygiene — tools and policies that respect the humans actually using them.</p>
        </div>
      </div>
    </div>
  </section>

  <!-- APPROACH -->
  <section id="approach">
    <div class="wrap">
      <div class="approach grain reveal">
        <span class="eyebrow">// how i work //</span>
        <p class="big">Start with the people and the problem. The smallest change that makes the biggest difference usually wins.</p>
        <div class="steps">
          <div class="step">
            <div class="k">01 — listen</div>
            <h3>Understand the real problem</h3>
            <p>What&rsquo;s actually slowing your team down — not just the symptom that landed in the ticket.</p>
          </div>
          <div class="step">
            <div class="k">02 — simplify</div>
            <h3>Right-size the fix</h3>
            <p>No gold-plating, no rip-and-replace for its own sake. The leanest solution that lasts.</p>
          </div>
          <div class="step">
            <div class="k">03 — hand off</div>
            <h3>Leave it better than I found it</h3>
            <p>Documented, maintainable, and owned by your team — not dependent on me forever.</p>
          </div>
        </div>
      </div>
    </div>
  </section>

  <!-- WORK -->
  <section id="work">
    <div class="wrap">
      <div class="sec-head reveal">
        <span class="eyebrow">// selected work //</span>
        <h2>Things I&rsquo;ve built and rescued.</h2>
        <p>A mix of systems shipped end-to-end and infrastructure quietly kept standing under pressure.</p>
      </div>
      <div class="work-grid">

        <article class="case reveal">
          <div class="shot"><img src="images/project-badge.webp" alt="Self-service badge printing web app" loading="lazy" width="1200" height="987"></div>
          <div class="body">
            <h3>Self-service badging</h3>
            <p class="meta"><b>purpose</b> &lt;print visitor &amp; employee badges on demand, no IT in the loop&gt;<br><b>tech</b> &lt;php; jquery; ec2&gt;</p>
          </div>
        </article>

        <article class="case reveal">
          <div class="shot"><img src="images/project-scheduler.webp" alt="Staff shift scheduling application" loading="lazy" width="1400" height="882"></div>
          <div class="body">
            <h3>Staff scheduler</h3>
            <p class="meta"><b>purpose</b> &lt;schedule &amp; manage shifts across a 24/7 operation&gt;<br><b>tech</b> &lt;php; mysql; jquery; ec2&gt;</p>
          </div>
        </article>

        <article class="case reveal">
          <div class="shot"><img src="images/project-packages.webp" alt="Real-time package tracking display" loading="lazy" width="1400" height="607"></div>
          <div class="body">
            <h3>Package tracking</h3>
            <p class="meta"><b>purpose</b> &lt;track packages for guests &amp; staff on a live display&gt;<br><b>tech</b> &lt;php; mysql; jquery; ec2&gt;</p>
          </div>
        </article>

        <article class="case text-only reveal">
          <div class="body">
            <h3>Global technology stack</h3>
            <p class="lede">Led infrastructure, applications and data for a global nonprofit — so the organization could focus on its mission, not its uptime.</p>
            <p class="meta"><b>scope</b> &lt;infra · apps · data&gt; &nbsp; <b>impact</b> &lt;one reliable foundation, worldwide&gt;</p>
          </div>
        </article>

        <article class="case text-only reveal">
          <div class="body">
            <h3>Edge hardening &amp; DDoS defense</h3>
            <p class="lede">Diagnosed and absorbed a flood of 40M+ malicious requests at the edge without taking the site down for real users.</p>
            <p class="meta"><b>tech</b> &lt;cloudflare; rate-limiting; managed challenge; waiting room&gt;</p>
            <p class="meta"><a href="/work/ddos-defense">read the case study &rarr;</a></p>
          </div>
        </article>

        <article class="case text-only reveal">
          <div class="body">
            <h3>Identity &amp; SSO migration</h3>
            <p class="lede">Designed a lean pipeline to move thousands of user profiles and photos into Okta-backed single sign-on — a clean bridge, not a forklift.</p>
            <p class="meta"><b>tech</b> &lt;cloudflare workers; r2; okta&gt;</p>
          </div>
        </article>

      </div>
    </div>
  </section>

  <!-- LIVE DOMAIN CHECK -->
  <section id="check">
    <div class="wrap">
      <div class="diag grain reveal">
        <span class="eyebrow">// live demo · runs in your browser //</span>
        <h2>Check any domain&rsquo;s email &amp; DNS health.</h2>
        <p class="sub">The kind of quick audit I run on day one, wired into this page. It reads public DNS records — SPF, DMARC, DNSSEC, MTA-STS and friends — and grades what it finds.</p>
        <form class="diag-form" id="diagForm">
          <label class="field"><span class="p" aria-hidden="true">$ audit</span>
            <input id="diagInput" type="text" inputmode="url" placeholder="yourcompany.com" autocomplete="off" autocapitalize="off" spellcheck="false" aria-label="Domain to check">
          </label>
          <button class="btn-run" id="diagRun" type="submit">Run check</button>
        </form>
        <p class="diag-note">queries go straight from your browser to Cloudflare DNS (1.1.1.1) — nothing is sent to, or logged by, this site.</p>
        <div class="diag-scan" id="diagScan" aria-hidden="true"><i></i></div>
        <div class="diag-out" id="diagOut" aria-live="polite">
          <div class="diag-grade"><div class="g" id="diagGrade">–</div><div class="s" id="diagScore"></div></div>
          <div class="diag-rows" id="diagRows"></div>
        </div>
        <p class="diag-cta" style="margin-top:14px">Prefer a guided version? Take the <a href="/audit">two-minute stack self-audit</a>.</p>
        <p class="diag-cta" id="diagCta" hidden>Want the full picture — DKIM selectors, TLS-RPT, alignment, the fix list? <a href="mailto:say@hello.edcho.xyz?subject=Domain%20audit">Ask for a real audit &rarr;</a></p>
      </div>
    </div>
  </section>

  <!-- ABOUT -->
  <section id="about">
    <div class="wrap about-grid">
      <div class="collage reveal">
        <img class="tall" src="images/adv-hiking.webp" alt="Ed hiking by an alpine lake" loading="lazy" width="900" height="1100">
        <img src="images/adv-rafting.webp" alt="Ed whitewater rafting with a group" loading="lazy" width="1200" height="800">
        <img src="images/work-rmhc.webp" alt="Ed at a nonprofit conference" loading="lazy" width="1000" height="750">
        <img class="tall" src="images/adv-running.webp" alt="Ed running a race across a bridge" loading="lazy" width="900" height="1100">
        <img src="images/adv-jet.webp" alt="Ed sitting inside a jet engine cowling" loading="lazy" width="900" height="1100">
        <img src="images/adv-wrigley.webp" alt="Ed with friends at a Chicago ballgame" loading="lazy" width="1200" height="800">
      </div>
      <div class="about-copy reveal">
        <span class="eyebrow">// who you&rsquo;d be working with //</span>
        <h2>Hi, I&rsquo;m Ed.</h2>
        <p>I&rsquo;m a Chicago-based technologist who&rsquo;s equally comfortable wrangling servers, writing code, or leading a team. I spent the bulk of my career making technology genuinely serve the people who depend on it — where reliability isn&rsquo;t about uptime charts, it&rsquo;s about real people getting what they need.</p>
        <p>I think about privacy every day — not as a rejection of technology, but as a belief that people deserve tools that respect them. When I&rsquo;m offline, I&rsquo;m usually somewhere between a backpacking trail, a bizarre plane route I found on points, and the finish line of a race that questions my life choices around mile eight.</p>
        <div class="creds">
          <span class="cred">B.S. Computer &amp; Electrical Engineering, Rutgers</span>
          <span class="cred">Speaker, ShoreTel One Partner Conference</span>
          <span class="cred">15+ years in nonprofit &amp; enterprise IT</span>
        </div>
      </div>
    </div>
  </section>

  <!-- CONTACT -->
  <section id="contact" class="contact">
    <div class="wrap">
      <div class="contact-card reveal">
        <span class="eyebrow">// let&rsquo;s talk //</span>
        <h2>Have a system that should just work?</h2>
        <p>Tell me what&rsquo;s slowing you down. We&rsquo;ll figure out the smallest change that makes the biggest difference — no obligation, no jargon.</p>
        <div class="contact-actions">
          <a class="btn btn-primary" href="mailto:say@hello.edcho.xyz?subject=Consulting%20inquiry">Email me &rarr;</a>
          <a class="btn btn-ghost" href="https://www.linkedin.com/in/helloed" rel="me noopener" target="_blank">Connect on LinkedIn</a>
        </div>
        <div class="contact-meta">
          <a href="mailto:say@hello.edcho.xyz">say@hello.edcho.xyz</a>
          <a href="https://www.linkedin.com/in/helloed" rel="me noopener" target="_blank">linkedin.com/in/helloed</a>
          <a href="https://bsky.app/profile/edcho.xyz" rel="me noopener" target="_blank">bsky.app/profile/edcho.xyz</a>
          <span>Chicago, IL</span>
          <a href="pgp.txt">PGP key</a>
          <span>Signal on request</span>
        </div>
      </div>
    </div>
  </section>

</main>

<footer class="foot">
  <div class="wrap foot-in">
    <span class="mono">// ed cho · chicago <span id="chiTime"></span>· type &#96; for a shell · <a href="/how">how this site is built</a> //</span>
    <div class="foot-soc">
      <a href="https://www.linkedin.com/in/helloed" target="_blank" rel="me noopener" aria-label="LinkedIn"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M4.98 3.5A2.5 2.5 0 1 1 0 3.5a2.5 2.5 0 0 1 4.98 0ZM.5 8h4v12.5h-4V8Zm6.5 0h3.8v1.7h.05c.53-1 1.83-2.05 3.77-2.05 4.03 0 4.78 2.65 4.78 6.1v6.75h-4v-6c0-1.43-.03-3.27-2-3.27-2 0-2.3 1.56-2.3 3.17v6.1H7V8Z"/></svg></a>
      <a href="https://bsky.app/profile/edcho.xyz" target="_blank" rel="me noopener" aria-label="Bluesky"><svg viewBox="0 0 568 501" fill="currentColor" aria-hidden="true"><path d="M123.121 33.664C188.241 82.553 258.281 181.68 284 234.873c25.719-53.193 95.759-152.32 160.879-201.21C491.866-1.611 568-28.906 568 57.947c0 17.346-9.945 145.713-15.778 166.555-20.275 72.453-94.155 90.933-159.875 79.748 114.875 19.55 144.097 84.31 80.986 149.071-119.86 122.992-172.272-30.859-185.702-70.281-2.462-7.227-3.614-10.608-3.631-7.733-.017-2.875-1.169.506-3.631 7.733-13.43 39.422-65.842 193.273-185.702 70.281-63.111-64.761-33.889-129.521 80.986-149.071-65.72 11.185-139.6-7.295-159.875-79.748C9.945 203.66 0 75.293 0 57.947 0-28.906 76.134-1.611 123.121 33.664Z"/></svg></a>
      <a href="mailto:say@hello.edcho.xyz" aria-label="Email"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg></a>
      <a href="pgp.txt" aria-label="PGP public key"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="4"/><path d="m11 11 9 9M16 16l2-2M19 19l1.5-1.5"/></svg></a>
    </div>
  </div>
</footer>

<div class="term" id="term" hidden>
  <div class="term-box" role="dialog" aria-modal="true" aria-label="Ed Cho interactive terminal">
    <div class="term-bar">
      <span class="term-dots" aria-hidden="true"><i></i><i></i><i></i></span>
      <span class="term-title">ec — bash</span>
      <button class="term-x" type="button" aria-label="Close terminal">esc</button>
    </div>
    <div class="term-body" id="termBody" tabindex="0" aria-live="polite"></div>
    <form class="term-form" id="termForm">
      <span class="term-prompt" aria-hidden="true">ed<!-- -->@<!-- -->edcho.xyz:~$</span>
      <input id="termInput" type="text" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="Terminal command input">
    </form>
  </div>
</div>

<script>
(function(){
  var t=document.querySelector('.nav-toggle'), m=document.getElementById('menu');
  if(t){t.addEventListener('click',function(){var o=m.classList.toggle('open');t.setAttribute('aria-expanded',o)});
    m.querySelectorAll('a').forEach(function(a){a.addEventListener('click',function(){m.classList.remove('open');t.setAttribute('aria-expanded',false)})});}
  var ct=document.getElementById('chiTime');
  if(ct){var tick=function(){try{ct.textContent='\u00b7 '+new Intl.DateTimeFormat('en-US',{hour:'2-digit',minute:'2-digit',hour12:false,timeZone:'America/Chicago'}).format(new Date())+' ct ';}catch(e){}};tick();setInterval(tick,30000);}
  var els=document.querySelectorAll('.reveal');
  if('IntersectionObserver' in window){
    var io=new IntersectionObserver(function(en){en.forEach(function(e){if(e.isIntersecting){e.target.classList.add('in');io.unobserve(e.target)}})},{threshold:.12,rootMargin:'0px 0px -40px 0px'});
    els.forEach(function(el,i){ if(el.closest('.hero')){el.style.transitionDelay=(i*70)+'ms';} io.observe(el); });
  } else { els.forEach(function(el){el.classList.add('in')}); }
})();
</script>
<script>
/* Domain health check — DNS-over-HTTPS to Cloudflare (1.1.1.1), entirely client-side. */
(function(){
  function doh(name,type){
    return fetch('https://cloudflare-dns.com/dns-query?name='+encodeURIComponent(name)+'&type='+type,
      {headers:{accept:'application/dns-json'}}).then(function(r){return r.json();});
  }
  function txts(j){ // join split TXT strings, strip quotes
    return (j.Answer||[]).filter(function(a){return a.type===16;})
      .map(function(a){return a.data.replace(/^"|"$/g,'').replace(/"\s+"/g,'');});
  }
  function clean(raw){
    var d=(raw||'').trim().toLowerCase().replace(/^[a-z]+:\/\//,'').replace(/^.*@/,'').split(/[\/?#\s]/)[0].replace(/\.$/,'');
    return /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/.test(d) ? d : null;
  }
  window.ecAudit=function(raw){
    var d=clean(raw);
    if(!d) return Promise.reject(new Error('that doesn\u2019t look like a domain \u2014 try e.g. example.com'));
    return Promise.all([
      doh(d,'MX'), doh(d,'TXT'), doh('_dmarc.'+d,'TXT'), doh(d,'A'),
      doh('_mta-sts.'+d,'TXT'), doh('default._bimi.'+d,'TXT'), doh(d,'CAA')
    ]).then(function(r){
      var rows=[], score=0;
      // MX
      var mx=(r[0].Answer||[]).filter(function(a){return a.type===15;});
      if(mx.length){score+=10; rows.push({k:'mx', s:'pass', v:'<b>'+mx.length+' record'+(mx.length>1?'s':'')+'</b> \u2014 mail routing configured'});}
      else rows.push({k:'mx', s:'warn', v:'<b>none</b> \u2014 no inbound mail for this domain', fix:'fine if the domain doesn\u2019t receive mail'});
      // SPF
      var spf=txts(r[1]).filter(function(t){return /^v=spf1/i.test(t);});
      if(spf.length===1){
        var hard=/-all\b/.test(spf[0]), soft=/~all\b/.test(spf[0]);
        score+= hard?20 : soft?17 : 12;
        rows.push({k:'spf', s: hard||soft?'pass':'warn', v:'<b>'+(hard?'-all (hard fail)':soft?'~all (soft fail)':'present, weak policy')+'</b>', fix: hard?null:'finish with -all so forged mail is rejected'});
      } else if(spf.length>1) rows.push({k:'spf', s:'fail', v:'<b>'+spf.length+' SPF records</b> \u2014 multiple records are invalid', fix:'merge into a single v=spf1 record'});
      else rows.push({k:'spf', s:'fail', v:'<b>missing</b> \u2014 anyone can forge mail from this domain', fix:'publish a v=spf1 TXT record'});
      // DMARC
      var dm=txts(r[2]).filter(function(t){return /^v=DMARC1/i.test(t);});
      if(dm.length){
        var p=(dm[0].match(/\bp=(\w+)/i)||[])[1]||'none', pct=(dm[0].match(/\bpct=(\d+)/i)||[])[1];
        var enforced=/^(quarantine|reject)$/i.test(p) && (!pct || pct==='100');
        score+= enforced?30 : (/^(quarantine|reject)$/i.test(p)?20:12);
        rows.push({k:'dmarc', s: enforced?'pass':'warn', v:'<b>p='+p.toLowerCase()+(pct?' pct='+pct:'')+'</b>'+(enforced?' \u2014 enforced':''), fix: enforced?null:'move to p=quarantine or p=reject at pct=100'});
      } else rows.push({k:'dmarc', s:'fail', v:'<b>missing</b> \u2014 no policy for failed authentication', fix:'publish _dmarc TXT, start at p=none, ratchet up'});
      // DNSSEC (AD flag)
      if(r[3].AD){score+=15; rows.push({k:'dnssec', s:'pass', v:'<b>validated</b> \u2014 responses are cryptographically signed'});}
      else rows.push({k:'dnssec', s:'warn', v:'<b>not signed</b> \u2014 DNS answers can\u2019t be verified', fix:'enable DNSSEC at your registrar/DNS host'});
      // MTA-STS
      if(txts(r[4]).some(function(t){return /^v=STSv1/i.test(t);})){score+=10; rows.push({k:'mta-sts', s:'pass', v:'<b>present</b> \u2014 inbound TLS is enforced'});}
      else rows.push({k:'mta-sts', s:'warn', v:'<b>missing</b> \u2014 mail TLS is best-effort only', fix:'nice-to-have: publish MTA-STS policy'});
      // BIMI (informational)
      if(txts(r[5]).some(function(t){return /^v=BIMI1/i.test(t);})){score+=7; rows.push({k:'bimi', s:'pass', v:'<b>present</b> \u2014 brand logo declared for inboxes'});}
      else rows.push({k:'bimi', s:'info', v:'not set \u2014 optional brand logo record'});
      // CAA (informational)
      if((r[6].Answer||[]).some(function(a){return a.type===257;})){score+=8; rows.push({k:'caa', s:'pass', v:'<b>present</b> \u2014 certificate issuance is restricted'});}
      else rows.push({k:'caa', s:'info', v:'not set \u2014 any CA may issue certs', fix:'optional: add a CAA record for your CA'});
      // DKIM note
      rows.push({k:'dkim', s:'info', v:'selector-specific \u2014 needs your sending setup; covered in a full audit'});
      var grade= score>=90?'A' : score>=75?'B' : score>=60?'C' : score>=45?'D' : 'F';
      return {domain:d, rows:rows, score:score, grade:grade};
    });
  };

  /* panel wiring */
  var form=document.getElementById('diagForm');
  if(!form) return;
  var input=document.getElementById('diagInput'), btn=document.getElementById('diagRun'),
      out=document.getElementById('diagOut'), rowsEl=document.getElementById('diagRows'),
      gEl=document.getElementById('diagGrade'), sEl=document.getElementById('diagScore'),
      scan=document.getElementById('diagScan'), cta=document.getElementById('diagCta');
  form.addEventListener('submit',function(e){
    e.preventDefault();
    btn.disabled=true; scan.classList.add('on'); out.classList.remove('on'); cta.hidden=true;
    window.ecAudit(input.value).then(function(res){
      rowsEl.innerHTML=res.rows.map(function(r){
        return '<div class="drow '+r.s+'"><span class="led" aria-hidden="true"></span><span class="k">'+r.k+
               '</span><span class="v">'+r.v+(r.fix?'<span class="fix">\u2192 '+r.fix+'</span>':'')+'</span></div>';
      }).join('');
      gEl.textContent=res.grade;
      gEl.className='g'+(res.grade==='A'||res.grade==='B'?'':(res.grade==='C'?' warn':' bad'));
      sEl.textContent=res.domain+' \u00b7 '+res.score+'/100';
      out.classList.add('on'); cta.hidden=false;
      cta.querySelector('a').href='mailto:say@hello.edcho.xyz?subject='+encodeURIComponent('Domain audit \u2014 '+res.domain);
    }).catch(function(err){
      rowsEl.innerHTML='<div class="drow fail"><span class="led" aria-hidden="true"></span><span class="k">error</span><span class="v">'+
        (err&&err.message?err.message:'couldn\u2019t reach the DNS resolver \u2014 try again')+'</span></div>';
      gEl.textContent='\u2013'; gEl.className='g'; sEl.textContent='';
      out.classList.add('on');
    }).finally(function(){ btn.disabled=false; scan.classList.remove('on'); });
  });
})();
</script>
<script>
(function(){
  var term=document.getElementById('term'), body=document.getElementById('termBody'),
      form=document.getElementById('termForm'), input=document.getElementById('termInput'),
      openBtn=document.getElementById('termOpen'), closeBtn=term.querySelector('.term-x'),
      last=null, started=false, hist=[], hi=0;
  function esc(s){return s.replace(/[&<>]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;'}[c];});}
  function line(html,cls){var p=document.createElement('p');p.className='term-line'+(cls?' '+cls:'');p.innerHTML=html;body.appendChild(p);body.scrollTop=body.scrollHeight;}
  var C={
    help:function(){return "<span class='muted'>commands:</span>\n  whoami    who is ed\n  services  what i can do for you\n  work      selected projects\n  stack     tools i reach for\n  audit     check a domain’s dns & email health\n  contact   how to reach me\n  hire      let\u2019s talk\n  clear     clear the screen\n<span class='muted'>  \u2026 a few more are hidden. poke around.</span>";},
    whoami:function(){return "Ed Cho \u2014 Chicago-based technology consultant. 15+ years turning fragile\nsystems into ones that quietly just work. Equally at home wrangling\nservers, writing code, or leading a team.";},
    services:function(){return "<span class='teal'>cloud & infrastructure</span>   Cloudflare, AWS, migrations, resilience\n<span class='teal'>security & identity</span>     Okta, SSO, access, DDoS mitigation\n<span class='teal'>fractional tech lead</span>    strategy, vendors, teams\n<span class='teal'>privacy & data</span>          people-first privacy";},
    work:function(){return "\u2022 self-service badging      php \u00b7 jquery \u00b7 ec2\n\u2022 staff scheduler           php \u00b7 mysql \u00b7 jquery \u00b7 ec2\n\u2022 package tracking          live display\n\u2022 global nonprofit stack    infra \u00b7 apps \u00b7 data\n\u2022 edge hardening            absorbed 40M+ malicious requests\n\u2022 okta SSO migration        workers \u00b7 r2 \u00b7 okta\n<span class='muted'>scroll to the Work section for the full story.</span>";},
    stack:function(){return "aws \u00b7 azure \u00b7 cloudflare \u00b7 jira \u00b7 microsoft 365\nnext.js \u00b7 okta \u00b7 php \u00b7 salesforce \u00b7 sql \u00b7 wordpress";},
    contact:function(){return "email     <a href='mailto:say@hello.edcho.xyz'>say@hello.edcho.xyz</a>\nlinkedin  <a href='https://www.linkedin.com/in/helloed' target='_blank' rel='noopener'>/in/helloed</a>\nbluesky   <a href='https://bsky.app/profile/edcho.xyz' target='_blank' rel='noopener'>@edcho.xyz</a>\npgp       <a href='pgp.txt'>/pgp.txt</a>";},
    hire:function(){setTimeout(function(){location.href='mailto:say@hello.edcho.xyz?subject=Let%27s%20talk';},700);return "<span class='teal'>booting hiring sequence\u2026</span> \u2713\nopening a message to ed \u2014 talk soon.";},
    audit:function(a){
      if(!a[0]) return "usage: audit <domain>   e.g. <span class='teal'>audit edcho.xyz</span>";
      var target=a[0];
      window.ecAudit(target).then(function(res){
        var out=res.rows.map(function(r){
          var c=r.s==='pass'?'teal':(r.s==='fail'?'amber':'muted');
          return "  <span class='"+c+"'>"+(r.s==='pass'?'\u2713':r.s==='fail'?'\u2717':'\u25cb')+"</span> "+r.k+"  "+r.v.replace(/<[^>]+>/g,'');
        }).join("\n");
        line(out); line("grade: <span class='"+(res.grade==='A'||res.grade==='B'?'teal':'amber')+"'>"+res.grade+"</span> ("+res.score+"/100) \u2014 <a href='mailto:say@hello.edcho.xyz?subject="+encodeURIComponent('Domain audit \u2014 '+res.domain)+"'>ask for the full audit</a>");
      }).catch(function(e){ line("audit: "+(e&&e.message?e.message:'lookup failed'),'muted'); });
      return "<span class='muted'>querying 1.1.1.1 over DoH\u2026</span>";
    },
    clear:function(){body.innerHTML='';return null;},
    hobbies:function(){return "backpacking/   travel-hacking/   obstacle-races/   mine-races/   baseball/";},
    ls:function(a){return (a[0]&&/hobb/.test(a[0]))?C.hobbies():"about   services   work   contact   hobbies";},
    uptime:function(){return "up 15+ years, 0 unplanned outages of enthusiasm. load average: caffeinated.";},
    coffee:function(){return "\u2615 brewing\u2026 done. <span class='muted'>(ed runs on cold brew and points redemptions.)</span>";},
    echo:function(a){return esc(a.join(' '));},
    sudo:function(a){return a.join(' ').indexOf('hire')>-1 ? C.hire() : "<span class='amber'>nice try.</span> you already have access to ed\u2019s calendar \u2014 just <a href='mailto:say@hello.edcho.xyz'>email</a>.";}
  };
  C.about=C.whoami; C.projects=C.work; C.tools=C.stack;
  function run(raw){
    var parts=raw.trim().split(/\s+/), cmd=(parts.shift()||'').toLowerCase();
    line("<span class='muted'>ed@edcho.xyz:~$</span> <span class='cmd'>"+esc(raw)+"</span>");
    if(!cmd) return;
    if(cmd==='hire-ed'||cmd==='hireme') cmd='hire';
    if(C[cmd]){var out=C[cmd](parts); if(out!=null) line(out);}
    else line("command not found: "+esc(cmd)+" \u2014 try <span class='teal'>help</span>",'muted');
  }
  function open(){ last=document.activeElement; term.hidden=false;
    if(!started){started=true; line("<span class='teal'>ec</span> \u2014 interactive shell. type <span class='teal'>help</span> to begin. <span class='muted'>(esc to close)</span>");}
    setTimeout(function(){input.focus();},20); }
  function close(){ term.hidden=true; if(last&&last.focus) last.focus(); }
  if(openBtn) openBtn.addEventListener('click',open);
  closeBtn.addEventListener('click',close);
  term.addEventListener('mousedown',function(e){ if(e.target===term) close(); });
  form.addEventListener('submit',function(e){ e.preventDefault(); var v=input.value; if(v.trim()){hist.push(v);hi=hist.length;} run(v); input.value=''; });
  input.addEventListener('keydown',function(e){
    if(e.key==='ArrowUp'&&hi>0){hi--;input.value=hist[hi]||'';e.preventDefault();}
    else if(e.key==='ArrowDown'&&hi<hist.length){hi++;input.value=hist[hi]||'';}
  });
  document.addEventListener('keydown',function(e){
    if(e.key==='Escape'&&!term.hidden){close();return;}
    if(e.key==='`'&&term.hidden){var t=document.activeElement,n=t&&t.tagName;if(n!=='INPUT'&&n!=='TEXTAREA'){e.preventDefault();open();}}
  });
})();
</script>
</body>
</html>
