Initial commit: site + GA4 + Docker for Coolify
|
After Width: | Height: | Size: 134 KiB |
@@ -0,0 +1,65 @@
|
||||
/* Inter Tight (SIL Open Font License 1.1) - self-hosted, no Google Fonts request */
|
||||
@font-face {
|
||||
font-family: 'Inter Tight';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 300;
|
||||
src: url(fonts/inter-tight-latin-ext-300-normal.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Tight';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 300;
|
||||
src: url(fonts/inter-tight-latin-300-normal.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Tight';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url(fonts/inter-tight-latin-ext-400-normal.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Tight';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url(fonts/inter-tight-latin-400-normal.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Tight';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 500;
|
||||
src: url(fonts/inter-tight-latin-ext-500-normal.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Tight';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 500;
|
||||
src: url(fonts/inter-tight-latin-500-normal.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Tight';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 600;
|
||||
src: url(fonts/inter-tight-latin-ext-600-normal.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Tight';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 600;
|
||||
src: url(fonts/inter-tight-latin-600-normal.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/* Homepage: option 2 sections (3D, neon, fireworks) scoped under .o2.
|
||||
Panels that act as "screens" (venture stage, fireworks stage) stay dark in
|
||||
both themes; everything else reads from the shared tokens. */
|
||||
.o2 {
|
||||
--o2-text: var(--ink);
|
||||
--o2-muted: var(--slate);
|
||||
--o2-faint: var(--smoke);
|
||||
--neon-off: #222c47;
|
||||
--neon-on: #e8f9ff;
|
||||
--neon-glow: 0 0 4px rgba(255,255,255,.9), 0 0 14px #1fb7ff, 0 0 34px #1a7fff, 0 0 72px rgba(26,127,255,.8);
|
||||
--neon-mid: #9fd8f2;
|
||||
--stroke-word: rgba(159, 230, 255, .14);
|
||||
--o2-tile-bd: rgba(159, 230, 255, .3);
|
||||
--o2-tile-bg: linear-gradient(145deg, rgba(159, 230, 255, .14), rgba(26, 127, 255, .04));
|
||||
--o2-beam: rgba(31, 183, 255, .30);
|
||||
}
|
||||
:root[data-theme="light"] .o2 {
|
||||
--neon-off: #c3cfe2;
|
||||
--neon-on: #0a5fd1;
|
||||
--neon-glow: 0 0 18px rgba(26, 127, 255, .35);
|
||||
--neon-mid: #6aa6e8;
|
||||
--stroke-word: rgba(10, 60, 140, .12);
|
||||
--o2-tile-bd: rgba(26, 127, 255, .25);
|
||||
--o2-tile-bg: linear-gradient(145deg, rgba(26, 127, 255, .10), rgba(255, 255, 255, .6));
|
||||
--o2-beam: rgba(31, 140, 255, .16);
|
||||
}
|
||||
.o2 > section { position: relative; overflow-x: clip; padding: clamp(5rem, 12vh, 8rem) 0; }
|
||||
.o2 .lead { color: var(--o2-muted); }
|
||||
.o2 .grad2 { background: linear-gradient(180deg, #a9dfff 0%, #5aa8ff 60%, #1a7fff); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
||||
:root[data-theme="light"] .o2 .grad2 { background: linear-gradient(180deg, #1a6fe0, #0b4fb0); -webkit-background-clip: text; background-clip: text; }
|
||||
|
||||
/* ---------- hero ---------- */
|
||||
#hero { min-height: calc(100svh - var(--nav-h)); display: flex; align-items: center; justify-content: center; text-align: center; padding: 3rem 0 6rem; overflow: hidden; }
|
||||
.beam { position: absolute; left: 50%; top: -10%; width: min(1100px, 140vw); height: 90%; transform: translateX(-50%); pointer-events: none;
|
||||
background: conic-gradient(from 180deg at 50% 0%, transparent 160deg, transparent 168deg, var(--o2-beam) 180deg, transparent 192deg, transparent 200deg);
|
||||
filter: blur(26px); opacity: .9; }
|
||||
.beam::after { content: ""; position: absolute; left: 50%; top: 0; width: 60%; height: 70%; transform: translateX(-50%); background: radial-gradient(closest-side, var(--glow), transparent); }
|
||||
.floaters .t { position: absolute; display: flex; flex-direction: column; align-items: center; gap: .5rem; top: var(--y); animation: drift 9s ease-in-out infinite; animation-delay: var(--d); }
|
||||
.floaters .t.l { left: var(--x); } .floaters .t.r { right: var(--x); }
|
||||
@keyframes drift { 50% { transform: translateY(-16px) rotate(var(--r, 0deg)); } }
|
||||
.tile { display: grid; place-items: center; width: 60px; height: 60px; border-radius: 18px; border: 1px solid var(--o2-tile-bd); background: var(--o2-tile-bg); color: var(--ice);
|
||||
box-shadow: 0 0 30px rgba(26, 127, 255, .25), inset 0 0 18px rgba(159, 230, 255, .08); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); transition: box-shadow .3s, border-color .3s; }
|
||||
.t:hover .tile { border-color: var(--sky); box-shadow: 0 0 44px rgba(31, 183, 255, .5), inset 0 0 18px rgba(159, 230, 255, .15); }
|
||||
.tile svg { width: 28px; height: 28px; }
|
||||
.tl { font-size: .74rem; color: var(--o2-muted); white-space: nowrap; }
|
||||
.icon-row { display: none; grid-template-columns: repeat(3, 1fr); gap: 1.2rem .8rem; max-width: 380px; margin: 2.4rem auto 0; }
|
||||
.icon-row .t { display: flex; flex-direction: column; align-items: center; gap: .45rem; }
|
||||
.icon-row .tl { white-space: normal; text-align: center; line-height: 1.2; }
|
||||
@media (max-width: 1179px) { .floaters { display: none; } .icon-row { display: grid; } }
|
||||
@media (min-width: 600px) and (max-width: 1179px) { .icon-row { grid-template-columns: repeat(6, 1fr); max-width: 660px; } }
|
||||
.emblem { position: relative; width: clamp(116px, 12vw, 164px); margin: 0 auto; perspective: 700px; }
|
||||
.emblem img { display: block; width: 100%; height: auto; transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)); transition: transform .3s ease-out; filter: drop-shadow(0 0 30px rgba(26, 127, 255, .65)); animation: float 6s ease-in-out infinite; }
|
||||
.emblem::after { content: ""; position: absolute; left: 10%; right: 10%; bottom: -14px; height: 18px; background: radial-gradient(closest-side, rgba(31, 183, 255, .55), transparent); filter: blur(6px); }
|
||||
@keyframes float { 50% { translate: 0 -10px; } }
|
||||
.hero-in { position: relative; z-index: 2; max-width: 1000px; padding-inline: var(--gutter); display: flex; flex-direction: column; align-items: center; }
|
||||
.hero-in > * { opacity: 0; transform: translateY(22px); filter: blur(8px); animation: heroIn 1s cubic-bezier(.2, .85, .2, 1) forwards; }
|
||||
.hero-in > *:nth-child(1) { animation-delay: .05s; } .hero-in > *:nth-child(2) { animation-delay: .25s; } .hero-in > *:nth-child(3) { animation-delay: .45s; }
|
||||
.hero-in > *:nth-child(4) { animation-delay: .65s; } .hero-in > *:nth-child(5) { animation-delay: .85s; } .hero-in > *:nth-child(6) { animation-delay: 1.05s; }
|
||||
@keyframes heroIn { to { opacity: 1; transform: none; filter: none; } }
|
||||
#hero .eyebrow { align-self: center; margin-top: 1rem; }
|
||||
#hero h1 { margin: 1.2rem auto 1.3rem; font-weight: 500; font-size: clamp(2.4rem, 5.6vw, 5.2rem); line-height: 1; letter-spacing: -0.05em; text-wrap: balance; }
|
||||
#hero h1 .grad, #hero h1 .grad2 { display: inline; }
|
||||
#hero .lead { margin: 0 auto 2rem; font-size: clamp(1rem, 1.5vw, 1.18rem); max-width: 62ch; }
|
||||
#hero .btn-row { justify-content: center; }
|
||||
.scroll-note { position: absolute; bottom: 1.6rem; left: 50%; transform: translateX(-50%); }
|
||||
.scroll-note i { display: block; width: 1px; height: 46px; background: linear-gradient(var(--sky), transparent); animation: drip 2s infinite; }
|
||||
@keyframes drip { 0% { transform: scaleY(0); transform-origin: top; } 50% { transform: scaleY(1); transform-origin: top; } 51% { transform-origin: bottom; } 100% { transform: scaleY(0); transform-origin: bottom; } }
|
||||
|
||||
/* ---------- giant words + cube ---------- */
|
||||
.o2 > .wsec { min-height: min(100svh, 820px); padding: 0; overflow: hidden; }
|
||||
.wsec-in { position: absolute; inset: 0; perspective: 1200px; }
|
||||
.cube { --c: min(36vmin, 320px); position: absolute; left: 50%; top: 50%; width: var(--c); height: var(--c); margin: calc(var(--c) / -2) 0 0 calc(var(--c) / -2); transform-style: preserve-3d; animation: spin 26s linear infinite; }
|
||||
.cube i { position: absolute; inset: 0; border: 1px solid rgba(159, 230, 255, .55); background: rgba(31, 183, 255, .05); box-shadow: inset 0 0 40px rgba(31, 183, 255, .12); }
|
||||
:root[data-theme="light"] .cube i { border-color: rgba(26, 127, 255, .45); }
|
||||
.cube i:nth-child(1) { transform: translateZ(calc(var(--c) / 2)); } .cube i:nth-child(2) { transform: rotateY(180deg) translateZ(calc(var(--c) / 2)); }
|
||||
.cube i:nth-child(3) { transform: rotateY(90deg) translateZ(calc(var(--c) / 2)); } .cube i:nth-child(4) { transform: rotateY(-90deg) translateZ(calc(var(--c) / 2)); }
|
||||
.cube i:nth-child(5) { transform: rotateX(90deg) translateZ(calc(var(--c) / 2)); } .cube i:nth-child(6) { transform: rotateX(-90deg) translateZ(calc(var(--c) / 2)); }
|
||||
@keyframes spin { to { transform: rotateX(360deg) rotateY(360deg); } }
|
||||
.cube-glow { position: absolute; left: 50%; top: 50%; width: 70vmin; height: 70vmin; transform: translate(-50%, -50%); background: radial-gradient(closest-side, rgba(26, 127, 255, .35), transparent); pointer-events: none; }
|
||||
:root[data-theme="light"] .cube-glow { background: radial-gradient(closest-side, rgba(26, 127, 255, .14), transparent); }
|
||||
.wmask { position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%); overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent); mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent); }
|
||||
#wTrack { display: inline-flex; gap: .35em; padding-right: .35em; white-space: nowrap; font-weight: 500; font-size: clamp(4.6rem, 17vw, 16rem); line-height: 1.05; letter-spacing: -0.06em; will-change: transform; }
|
||||
#wTrack span { background: linear-gradient(180deg, var(--grad-a) 10%, #7fcdff 62%, #1a7fff); -webkit-background-clip: text; background-clip: text; color: transparent; transition: opacity .15s; }
|
||||
.w-side { position: absolute; right: max(var(--gutter), calc((100vw - var(--page-max)) / 2 + var(--gutter))); bottom: clamp(2rem, 9vh, 5rem); max-width: 320px; display: flex; flex-direction: column; gap: .9rem; align-items: flex-start; z-index: 3; }
|
||||
.w-side p { color: var(--o2-muted); font-size: .98rem; }
|
||||
.w-top { position: absolute; left: max(var(--gutter), calc((100vw - var(--page-max)) / 2 + var(--gutter))); top: 2.5rem; z-index: 3; }
|
||||
@media (max-width: 700px) { .w-side { left: var(--gutter); right: var(--gutter); max-width: none; } }
|
||||
|
||||
/* ---------- building: copy beside the portfolio dashboard ---------- */
|
||||
.bgrid { display: grid; grid-template-columns: 0.75fr 1.25fr; gap: clamp(2rem, 5vw, 4.5rem); align-items: center; }
|
||||
.bgrid > * { min-width: 0; }
|
||||
.bhead { display: flex; flex-direction: column; gap: 1.4rem; align-items: flex-start; }
|
||||
.cluster-fit { position: relative; width: 100%; }
|
||||
.cluster-fit .cluster { margin: 0; width: 760px; height: 740px; max-width: none; transform-origin: 0 0; transform: scale(var(--s, 1)); }
|
||||
.cluster-fit .dash { left: 176px; top: 0; width: 540px; }
|
||||
.cluster-fit .profile { left: 0; top: 210px; width: 196px; }
|
||||
.cluster-fit .code { right: 0; top: 420px; width: 320px; }
|
||||
.cluster-fit .dash-cols { grid-template-columns: 1.25fr 1fr; }
|
||||
.cluster-fit .row { font-size: 12.5px; }
|
||||
@media (max-width: 1100px) {
|
||||
.bgrid { grid-template-columns: 1fr; }
|
||||
.cluster-fit .cluster { width: auto; height: auto; transform: none; }
|
||||
.cluster-fit .panel { left: auto; right: auto; top: auto; width: auto; }
|
||||
.cluster-fit .profile { max-width: 340px; }
|
||||
}
|
||||
|
||||
/* ---------- ventures: tabs + themed 3D object ---------- */
|
||||
.vhead { text-align: center; display: flex; flex-direction: column; align-items: center; gap: 1.1rem; margin-bottom: 2.4rem; }
|
||||
.vhead .lead { max-width: 60ch; }
|
||||
.vhead .eyebrow, .jhead .eyebrow { align-self: center; }
|
||||
.jhead .eyebrow { align-self: flex-start; }
|
||||
.vtabs { display: flex; gap: .4rem; padding: .35rem; border: 1px solid var(--bd); border-radius: 14px; background: var(--glass); overflow-x: auto; max-width: 100%; scrollbar-width: none; margin: 0 auto 1.6rem; width: max-content; }
|
||||
.vtabs::-webkit-scrollbar { display: none; }
|
||||
.vtabs button { padding: .55rem 1rem; border: 0; border-radius: 10px; background: none; font: 500 .88rem var(--font-sans); color: var(--o2-muted); white-space: nowrap; cursor: pointer; transition: all .25s; }
|
||||
.vtabs button:hover { color: var(--ink); }
|
||||
.vtabs button[aria-selected="true"] { background: var(--ink); color: var(--porcelain); }
|
||||
.vpanel { position: relative; display: grid; grid-template-columns: 1fr 1fr; min-height: 480px; border: 1px solid var(--bd); border-radius: 22px; color: #f3f7ff; overflow: hidden; --ac: #1fb7ff;
|
||||
--vp-title: #f3f7ff; --vp-text: #b3c0da; --vp-num: #55627f; --vp-lines: rgba(255, 255, 255, .04); --vp-glow: 40%; --vp-arrow-bg: rgba(10, 14, 26, .7); --vp-arrow-fg: #fff; --vp-arrow-bd: rgba(255, 255, 255, .09);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, .04), rgba(255, 255, 255, .01)), #070b16; }
|
||||
:root[data-theme="light"] .vpanel { --vp-title: #0b1220; --vp-text: #4a5873; --vp-num: #8896b3; --vp-lines: rgba(15, 23, 42, .05); --vp-glow: 28%; --vp-arrow-bg: rgba(255, 255, 255, .85); --vp-arrow-fg: #0b1220; --vp-arrow-bd: rgba(15, 23, 42, .12);
|
||||
background: linear-gradient(135deg, #ffffff, #eef4fd); box-shadow: 0 30px 60px -40px rgba(15, 23, 42, .35); }
|
||||
.vpanel::before { content: ""; position: absolute; inset: 0; background: radial-gradient(60% 80% at 75% 50%, color-mix(in srgb, var(--ac) var(--vp-glow), transparent), transparent 70%); transition: background .6s; pointer-events: none; }
|
||||
.vpanel::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(var(--vp-lines) 1px, transparent 1px) 0 0 / 100% 38px; -webkit-mask-image: linear-gradient(90deg, transparent 30%, #000); mask-image: linear-gradient(90deg, transparent 30%, #000); }
|
||||
.vinfo { position: relative; z-index: 2; padding: 2.4rem; display: flex; flex-direction: column; justify-content: space-between; gap: 2rem; }
|
||||
.vnum { font-size: .9rem; color: var(--vp-num); font-variant-numeric: tabular-nums; }
|
||||
.vinfo h3 { font-weight: 500; font-size: clamp(1.8rem, 3.4vw, 3rem); line-height: 1.05; letter-spacing: -0.045em; margin-bottom: 1rem; color: var(--vp-title); }
|
||||
.vinfo p { color: var(--vp-text); font-size: 1.08rem; max-width: 34ch; }
|
||||
.vinfo .vt { transition: opacity .3s, transform .3s; }
|
||||
.vinfo .vt.out { opacity: 0; transform: translateY(10px); }
|
||||
#vcanvas { position: relative; z-index: 1; width: 100%; height: 100%; min-height: 340px; cursor: grab; }
|
||||
.varrows { position: absolute; z-index: 3; right: 1.2rem; bottom: 1.2rem; display: flex; gap: .5rem; }
|
||||
.varrows button { width: 44px; height: 44px; border: 1px solid var(--vp-arrow-bd); border-radius: 10px; background: var(--vp-arrow-bg); color: var(--vp-arrow-fg); backdrop-filter: blur(8px); display: grid; place-items: center; cursor: pointer; transition: border-color .2s; }
|
||||
.varrows button:hover { border-color: #1fb7ff; }
|
||||
.varrows svg { width: 16px; height: 16px; }
|
||||
@media (max-width: 860px) { .vpanel { grid-template-columns: 1fr; grid-template-rows: 280px auto; } #vcanvas { order: -1; min-height: 280px; } .vinfo { padding: 1.6rem 1.6rem 4.6rem; } }
|
||||
|
||||
/* ---------- journey: fireworks + Dylan's stats ---------- */
|
||||
.jgrid { display: grid; grid-template-columns: 1fr 1.1fr; grid-template-areas: "head stage" "body stage"; gap: 1.6rem clamp(2rem, 5vw, 4rem); align-items: start; }
|
||||
.jgrid > * { min-width: 0; }
|
||||
.jhead { grid-area: head; display: flex; flex-direction: column; gap: 1.1rem; align-items: flex-start; }
|
||||
.jbody { grid-area: body; }
|
||||
.jstage { grid-area: stage; position: relative; height: clamp(460px, 78vh, 680px); align-self: stretch; overflow: hidden; border-radius: 18px; border: 1px solid var(--bd); color: var(--ink);
|
||||
--floor-line: rgba(159, 230, 255, .26); --pin: #1fb7ff; --pin-label: #55627f; --pin-done: #fff; --final: #d6e8ff;
|
||||
background: radial-gradient(70% 55% at 50% 100%, rgba(26, 127, 255, .18), transparent 70%), #060a14; }
|
||||
:root[data-theme="light"] .jstage { --floor-line: rgba(26, 111, 224, .28); --pin: #1a7fff; --pin-label: #8896b3; --pin-done: #0b1220; --final: #13306b;
|
||||
background: radial-gradient(70% 55% at 50% 100%, rgba(26, 127, 255, .14), transparent 70%), linear-gradient(180deg, #ffffff, #eef4fd); }
|
||||
.jlist { display: grid; gap: .7rem; }
|
||||
.jx { display: flex; gap: 1rem; align-items: flex-start; padding: 1rem 1.2rem; border: 1px solid var(--bd); border-radius: 14px; background: var(--glass); opacity: .4; transition: opacity .6s, border-color .6s, box-shadow .6s, background .6s; }
|
||||
.jx .ji { flex: none; display: grid; place-items: center; width: 44px; height: 44px; border-radius: 12px; border: 1px solid var(--bd); color: var(--o2-faint); transition: all .6s; }
|
||||
.jx svg { width: 24px; height: 24px; }
|
||||
.jx h3 { font-weight: 500; font-size: 1.15rem; letter-spacing: -0.03em; margin-bottom: .15rem; }
|
||||
.jx p { color: var(--o2-muted); font-size: .92rem; line-height: 1.45; }
|
||||
.jx.lit { opacity: 1; border-color: rgba(31, 183, 255, .5); background: linear-gradient(150deg, rgba(26, 127, 255, .14), var(--glass)); box-shadow: 0 0 36px rgba(26, 127, 255, .14); }
|
||||
.jx.lit .ji { color: var(--ice); border-color: rgba(31, 183, 255, .55); box-shadow: 0 0 18px rgba(31, 183, 255, .4); }
|
||||
.floor { position: absolute; left: -10%; right: -10%; bottom: -6%; height: 46%; transform: perspective(700px) rotateX(64deg); transform-origin: 50% 100%;
|
||||
background: linear-gradient(var(--floor-line) 1px, transparent 1px) 0 0 / 56px 56px, linear-gradient(90deg, var(--floor-line) 1px, transparent 1px) 0 0 / 56px 56px;
|
||||
-webkit-mask-image: radial-gradient(closest-side at 50% 70%, #000 35%, transparent); mask-image: radial-gradient(closest-side at 50% 70%, #000 35%, transparent); }
|
||||
.pins { position: absolute; left: 0; right: 0; bottom: 17%; height: 0; }
|
||||
.pn { position: absolute; bottom: 0; width: 0; }
|
||||
.pn i { position: absolute; left: 0; bottom: 0; width: 2px; height: 0; transform: translateX(-1px); background: linear-gradient(var(--pin), transparent); opacity: 0; }
|
||||
.pn.rise i { height: var(--ph, 100px); opacity: 1; transition: height .8s cubic-bezier(.25, .7, .35, 1), opacity .2s; }
|
||||
.pn b { position: absolute; left: -8px; bottom: var(--ph, 100px); width: 16px; height: 16px; margin-bottom: -8px; border-radius: 50%; background: #fff; border: 2px solid var(--pin); opacity: 0; transform: scale(.2); transition: opacity .3s, transform .5s cubic-bezier(.3, 1.6, .5, 1); }
|
||||
.pn.done b { opacity: 1; transform: scale(1); box-shadow: 0 0 24px 6px var(--pin); }
|
||||
.pn em { position: absolute; top: 20px; left: 0; transform: translateX(-50%); width: 84px; text-align: center; line-height: 1.15; font-style: normal; font-size: .76rem; color: var(--pin-label); transition: color .5s; }
|
||||
.pn.done em { color: var(--pin-done); }
|
||||
#fw { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
|
||||
.jfinal { position: absolute; left: 1.2rem; top: 1.2rem; max-width: 240px; font-size: .98rem; line-height: 1.35; color: var(--final); opacity: 0; transform: translateY(8px); transition: opacity .8s, transform .8s; }
|
||||
.jfinal.show { opacity: 1; transform: none; }
|
||||
.jreplay { position: absolute; right: 1rem; top: 1rem; opacity: 0; pointer-events: none; transition: opacity .4s; }
|
||||
.jreplay.show { opacity: 1; pointer-events: auto; }
|
||||
.o2 .stats b.pop { animation: pop .45s ease; }
|
||||
@keyframes pop { 40% { transform: scale(1.12); color: var(--sky); } }
|
||||
.o2 .stats { margin-top: 3.5rem; }
|
||||
.o2 .stats b { font-weight: 500; letter-spacing: -0.05em; }
|
||||
@media (max-width: 900px) { .jgrid { grid-template-columns: 1fr; grid-template-areas: "head" "stage" "body"; } .jstage { height: 420px; } }
|
||||
|
||||
/* ---------- about: neon words ---------- */
|
||||
.agrid { display: grid; grid-template-columns: 1.15fr .85fr; gap: clamp(2rem, 5vw, 5rem); align-items: center; }
|
||||
.agrid > * { min-width: 0; }
|
||||
.awords span { display: block; padding-bottom: .14em; margin-bottom: -.1em; font-weight: 500; font-size: clamp(2.6rem, 7vw, 6.4rem); line-height: .98; letter-spacing: -0.055em; }
|
||||
.awords .neon { color: var(--neon-off); }
|
||||
.awords.is-in .neon { animation: neonOn 2.6s steps(1, end) .7s forwards, neonHum 7s ease-in-out 3.5s infinite; }
|
||||
.awords.is-in .neon.n2 { animation-delay: 1.5s, 4.3s; }
|
||||
@keyframes neonOn {
|
||||
0% { color: var(--neon-off); text-shadow: none; }
|
||||
10% { color: var(--neon-on); text-shadow: var(--neon-glow); }
|
||||
14% { color: var(--neon-off); text-shadow: none; }
|
||||
26% { color: var(--neon-on); text-shadow: var(--neon-glow); }
|
||||
30% { color: var(--neon-off); text-shadow: none; }
|
||||
36% { color: var(--neon-mid); text-shadow: 0 0 10px rgba(31, 183, 255, .5); }
|
||||
40% { color: var(--neon-off); text-shadow: none; }
|
||||
52% { color: var(--neon-on); text-shadow: var(--neon-glow); }
|
||||
56% { color: var(--neon-mid); text-shadow: 0 0 8px #1fb7ff, 0 0 22px rgba(26, 127, 255, .7); }
|
||||
60%, 100% { color: var(--neon-on); text-shadow: var(--neon-glow); }
|
||||
}
|
||||
@keyframes neonHum { 0%, 45%, 55%, 100% { opacity: 1; } 48% { opacity: .78; } 50% { opacity: 1; } 52% { opacity: .9; } }
|
||||
.atext { display: flex; flex-direction: column; gap: 1.1rem; align-items: flex-start; }
|
||||
.atext .q { font-size: clamp(1.15rem, 1.9vw, 1.5rem); line-height: 1.3; letter-spacing: -0.02em; color: var(--ink); max-width: 36ch; }
|
||||
@media (max-width: 860px) { .agrid { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ---------- consulting cards ---------- */
|
||||
.chead { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: end; margin-bottom: 2.6rem; }
|
||||
.chead > * { min-width: 0; }
|
||||
.cgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
.cc { position: relative; padding: 1.8rem; border: 1px solid var(--bd); border-radius: 16px; background: linear-gradient(160deg, var(--glass), transparent), var(--cloud); overflow: hidden; min-height: 210px; transition: border-color .3s, box-shadow .3s; }
|
||||
.cc::before { content: ""; position: absolute; right: -40px; top: -40px; width: 200px; height: 200px; background: radial-gradient(closest-side, rgba(26, 127, 255, .28), transparent); transition: opacity .4s; opacity: .6; }
|
||||
.cc:hover { border-color: rgba(31, 183, 255, .5); box-shadow: 0 0 40px rgba(26, 127, 255, .12); }
|
||||
.cc .cic { position: relative; display: grid; place-items: center; width: 58px; height: 58px; margin-bottom: 1.6rem; border-radius: 50%; border: 1px solid rgba(31, 183, 255, .4); background: radial-gradient(circle at 35% 30%, #16315f, #08101f); box-shadow: 0 0 24px rgba(26, 127, 255, .4); color: #9fe6ff; }
|
||||
.cc .cic svg { width: 26px; height: 26px; }
|
||||
.cc h3 { position: relative; font-weight: 500; font-size: 1.3rem; letter-spacing: -0.03em; margin-bottom: .5rem; }
|
||||
.cc p { position: relative; color: var(--o2-muted); font-size: .98rem; max-width: 46ch; }
|
||||
@media (max-width: 860px) { .chead { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 700px) { .cgrid { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ---------- playbook: outline word + our 3D book ---------- */
|
||||
#playbook { overflow: hidden; background: linear-gradient(transparent, rgba(26, 127, 255, .06), transparent); }
|
||||
.pbword { position: absolute; left: 0; top: 50%; transform: translateY(-50%); white-space: nowrap; font-weight: 500; font-size: clamp(7rem, 26vw, 24rem); letter-spacing: -0.07em; line-height: 1; color: transparent; -webkit-text-stroke: 1px var(--stroke-word); pointer-events: none; will-change: transform; }
|
||||
.pbgrid { position: relative; display: grid; grid-template-columns: 1.1fr .9fr; gap: 3rem; align-items: center; }
|
||||
.pbgrid > * { min-width: 0; }
|
||||
.pbtext { display: flex; flex-direction: column; gap: 1.2rem; align-items: flex-start; }
|
||||
.o2 .book-stage .book { box-shadow: -12px 0 0 #091a40, 0 40px 90px rgba(26, 127, 255, .4), inset 0 0 0 1px rgba(255, 255, 255, .12); transform: rotateY(-18deg) rotateX(4deg); cursor: pointer; }
|
||||
.o2 .book-stage:hover .book, .o2 .book.tap { transform: rotateY(-4deg) rotateX(1deg) translateY(-6px); }
|
||||
.o2 .book-stage .book .sheen { position: absolute; inset: 0; border-radius: inherit; background: linear-gradient(115deg, transparent 40%, rgba(255, 255, 255, .14) 50%, transparent 60%); pointer-events: none; }
|
||||
@media (max-width: 860px) { .pbgrid { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ---------- press ---------- */
|
||||
.phead { display: flex; justify-content: space-between; align-items: end; gap: 2rem; flex-wrap: wrap; margin-bottom: 2.2rem; }
|
||||
.pgrid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
|
||||
.pt { position: relative; display: flex; flex-direction: column; justify-content: space-between; min-height: 260px; padding: 1.4rem; border: 1px solid var(--bd); border-radius: 16px; background: var(--glass); overflow: hidden; transition: border-color .3s, transform .3s; }
|
||||
.pt::before { content: ""; position: absolute; inset: 0; background: radial-gradient(80% 60% at 50% 120%, rgba(26, 127, 255, .4), transparent); opacity: .5; transition: opacity .4s; }
|
||||
.pt:hover { border-color: rgba(31, 183, 255, .5); transform: translateY(-4px); }
|
||||
.pt:hover::before { opacity: 1; }
|
||||
.pt h3 { position: relative; font-weight: 500; font-size: 1.5rem; letter-spacing: -0.04em; }
|
||||
.pt .arrow { position: relative; align-self: flex-end; display: grid; place-items: center; width: 38px; height: 38px; border: 1px solid var(--bd); border-radius: 10px; }
|
||||
.pt .arrow svg { width: 14px; height: 14px; }
|
||||
@media (max-width: 960px) { .pgrid { grid-template-columns: 1fr 1fr; } }
|
||||
@media (max-width: 520px) { .pgrid { grid-template-columns: 1fr; } .pt { min-height: 150px; } }
|
||||
|
||||
/* ---------- poetry ---------- */
|
||||
.o2 > #poetry { min-height: 100svh; display: flex; align-items: center; justify-content: center; text-align: center; overflow: hidden; }
|
||||
.arc { position: absolute; left: 50%; bottom: -62vw; width: 130vw; height: 130vw; transform: translateX(-50%); border-radius: 50%; border: 2px solid rgba(31, 183, 255, .65);
|
||||
box-shadow: 0 0 80px rgba(26, 127, 255, .55), inset 0 0 120px rgba(26, 127, 255, .35), 0 -2px 18px rgba(159, 230, 255, .7); background: radial-gradient(closest-side at 50% 0%, rgba(26, 127, 255, .18), transparent); }
|
||||
:root[data-theme="light"] .arc { box-shadow: 0 0 60px rgba(26, 127, 255, .25), inset 0 0 120px rgba(26, 127, 255, .15); }
|
||||
#poetry .in-wrap { position: relative; z-index: 2; max-width: 820px; padding-inline: var(--gutter); display: flex; flex-direction: column; align-items: center; gap: 1.4rem; }
|
||||
#poetry .eyebrow { align-self: center; }
|
||||
#poetry .poem-line { color: var(--graphite); font-size: 1.25rem; }
|
||||
|
||||
/* ---------- connect ---------- */
|
||||
.o2 > #connect { padding: clamp(3rem, 8vh, 5rem) 0 0; overflow: hidden; }
|
||||
.talk { position: relative; width: 100%; height: clamp(14rem, 38vw, 34rem); display: flex; justify-content: center; align-items: center; overflow: hidden; -webkit-mask-image: linear-gradient(transparent, #000 20%, #000 80%, transparent); mask-image: linear-gradient(transparent, #000 20%, #000 80%, transparent); }
|
||||
.ribbon { position: absolute; left: 50%; top: 50%; width: max(130%, 1500px); max-width: none; height: auto; transform: translate(-50%, -50%); animation: rib 14s ease-in-out infinite alternate; filter: blur(.35px) saturate(1.08); image-rendering: auto; }
|
||||
@keyframes rib { from { transform: translate(-52%, -48%) rotate(-3deg) scale(1); } to { transform: translate(-48%, -52%) rotate(3deg) scale(1.06); } }
|
||||
.talk h2 { position: relative; z-index: 2; font-weight: 600; font-size: clamp(5rem, 23vw, 22rem); line-height: .9; letter-spacing: -0.07em; color: #fff; mix-blend-mode: difference; white-space: nowrap; }
|
||||
:root[data-theme="light"] .talk h2 { mix-blend-mode: normal; color: #0b1220; text-shadow: 0 2px 30px rgba(255, 255, 255, .6); }
|
||||
.cbody { position: relative; z-index: 2; padding-inline: var(--gutter); display: flex; flex-direction: column; align-items: center; text-align: center; gap: 1.2rem; margin-top: 1rem; }
|
||||
.cbody .eyebrow { align-self: center; }
|
||||
.cbody h3 { font-weight: 500; font-size: clamp(1.8rem, 3.6vw, 3rem); letter-spacing: -0.045em; max-width: 22ch; line-height: 1.08; }
|
||||
.cbody .prose { max-width: 62ch; align-items: center; }
|
||||
.cbody .prose p { margin-inline: auto; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-in > * { opacity: 1; transform: none; filter: none; animation: none; }
|
||||
.awords .neon { color: var(--neon-on); text-shadow: var(--neon-glow); }
|
||||
}
|
||||
@media (max-width: 640px) { .cluster-fit .dash-cols { grid-template-columns: 1fr; } }
|
||||
@@ -0,0 +1,389 @@
|
||||
/* Homepage interactions ported from option 2: floating venture icons, giant
|
||||
words marquee, scroll-linked type, fireworks journey, themed 3D venture
|
||||
objects (three.js r128) and the scaled portfolio dashboard. */
|
||||
(() => {
|
||||
'use strict';
|
||||
const $ = (s, c = document) => c.querySelector(s);
|
||||
const $$ = (s, c = document) => Array.from(c.querySelectorAll(s));
|
||||
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
|
||||
/* ---------- hero icons: one per venture ---------- */
|
||||
const svg = (p) => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + p + '</svg>';
|
||||
const ICONS = [
|
||||
{ n: 'AI Software', side: 'l', x: '8%', y: '24%', r: '6deg', d: '0s', p: '<rect x="6" y="6" width="12" height="12" rx="2.5"/><path d="M9 3v3M15 3v3M9 18v3M15 18v3M3 9h3M3 15h3M18 9h3M18 15h3"/><path d="M9.6 15v-3.6L12 9.4l2.4 2V15M9.6 13.4h4.8"/>' },
|
||||
{ n: 'Marketing & AI Search', side: 'l', x: '15%', y: '50%', r: '-7deg', d: '-2s', p: '<circle cx="10.5" cy="10.5" r="6.2"/><path d="m15.2 15.2 5 5"/><path d="m7.6 12.6 2.2-2.4 1.8 1.6 2.6-3"/>' },
|
||||
{ n: 'Spirits', side: 'l', x: '6%', y: '73%', r: '8deg', d: '-4s', p: '<path d="M10 2.5h4M10.5 2.5v4.2L8.6 9.4a3 3 0 0 0-.6 1.8V20a1.5 1.5 0 0 0 1.5 1.5h5A1.5 1.5 0 0 0 16 20v-8.8a3 3 0 0 0-.6-1.8l-1.9-2.7V2.5"/><path d="M8 13.5h8v4H8z"/>' },
|
||||
{ n: 'Education Technology', side: 'r', x: '8%', y: '22%', r: '-6deg', d: '-1s', p: '<path d="m2.5 9.5 9.5-5 9.5 5-9.5 5z"/><path d="M6.5 12v4.2c0 1.2 2.5 2.8 5.5 2.8s5.5-1.6 5.5-2.8V12"/><path d="M21.5 9.5v6"/>' },
|
||||
{ n: 'Cybersecurity', side: 'r', x: '15%', y: '50%', r: '7deg', d: '-3s', p: '<path d="M12 3 4.5 6v5.5c0 4.5 3.2 7.8 7.5 9.5 4.3-1.7 7.5-5 7.5-9.5V6z"/><rect x="9" y="10.8" width="6" height="5" rx="1"/><path d="M10.2 10.8V9.4a1.8 1.8 0 0 1 3.6 0v1.4"/>' },
|
||||
{ n: 'E-Commerce', side: 'r', x: '6%', y: '73%', r: '-8deg', d: '-5s', p: '<path d="M5 8h14l-1 12H6z"/><path d="M9 8V7a3 3 0 0 1 6 0v1"/>' },
|
||||
];
|
||||
const tileHTML = (i) => '<div class="tile">' + svg(i.p) + '</div><span class="tl">' + i.n.replace('&', '&') + '</span>';
|
||||
const fl = $('#floaters'), ir = $('#iconRow');
|
||||
if (fl && ir) {
|
||||
ICONS.forEach((i) => {
|
||||
const t = document.createElement('div');
|
||||
t.className = 't ' + i.side;
|
||||
t.style.cssText = '--x:' + i.x + ';--y:' + i.y + ';--r:' + i.r + ';--d:' + i.d;
|
||||
t.innerHTML = tileHTML(i);
|
||||
fl.appendChild(t);
|
||||
const t2 = document.createElement('div');
|
||||
t2.className = 't';
|
||||
t2.innerHTML = tileHTML(i);
|
||||
ir.appendChild(t2);
|
||||
});
|
||||
}
|
||||
|
||||
/* emblem follows the cursor a little */
|
||||
const emblemImg = $('#emblem img');
|
||||
addEventListener('pointermove', (e) => {
|
||||
if (!emblemImg || reduce || e.pointerType !== 'mouse') return;
|
||||
const mx = (e.clientX / innerWidth) * 2 - 1, my = (e.clientY / innerHeight) * 2 - 1;
|
||||
emblemImg.style.setProperty('--ry', (mx * 16).toFixed(1) + 'deg');
|
||||
emblemImg.style.setProperty('--rx', (-my * 11).toFixed(1) + 'deg');
|
||||
}, { passive: true });
|
||||
|
||||
/* neon words switch on when they scroll into view */
|
||||
const awords = $('.awords');
|
||||
if (awords) new IntersectionObserver((es, o) => es.forEach((e) => { if (e.isIntersecting) { awords.classList.add('is-in'); o.disconnect(); } }), { threshold: .3 }).observe(awords);
|
||||
|
||||
/* ---------- portfolio dashboard: scale the fixed-size cluster to its column ---------- */
|
||||
const fit = $('.cluster-fit'), cl = fit && $('.cluster', fit);
|
||||
const sizeCluster = () => {
|
||||
if (!fit || !cl) return;
|
||||
if (innerWidth <= 1100) { fit.style.height = ''; cl.style.removeProperty('--s'); return; }
|
||||
const s = Math.min(1, fit.clientWidth / 760);
|
||||
cl.style.setProperty('--s', s.toFixed(4));
|
||||
fit.style.height = Math.round(740 * s) + 'px';
|
||||
};
|
||||
sizeCluster();
|
||||
addEventListener('resize', sizeCluster);
|
||||
|
||||
/* ---------- scroll-linked playbook word ---------- */
|
||||
const pbSec = $('#playbook'), pbWord = $('#pbword');
|
||||
let ticking = false;
|
||||
function onScroll() {
|
||||
if (pbSec && pbWord) {
|
||||
const rp = pbSec.getBoundingClientRect();
|
||||
const kp = clamp((innerHeight - rp.top) / (innerHeight + rp.height), 0, 1);
|
||||
pbWord.style.transform = 'translate3d(' + (reduce ? 0 : lerp(-6, -38, kp)).toFixed(1) + '%,-50%,0)';
|
||||
}
|
||||
}
|
||||
addEventListener('scroll', () => { if (!ticking) { ticking = true; requestAnimationFrame(() => { ticking = false; onScroll(); }); } }, { passive: true });
|
||||
addEventListener('resize', onScroll);
|
||||
onScroll();
|
||||
const book = $('#book');
|
||||
if (book) book.addEventListener('click', () => book.classList.toggle('tap'));
|
||||
|
||||
/* ---------- giant words: moves on its own ---------- */
|
||||
const wTrack = $('#wTrack'), wSpans = wTrack ? $$('span', wTrack) : [], wSec = $('#words');
|
||||
let wx = 0, wVis = false;
|
||||
if (wSec) new IntersectionObserver((es) => { wVis = es[0].isIntersecting; }).observe(wSec);
|
||||
function marquee(dt) {
|
||||
const half = wTrack.scrollWidth / 2;
|
||||
if (!half) return;
|
||||
wx -= (reduce ? 0 : 58) * dt;
|
||||
if (wx <= -half) wx += half;
|
||||
wTrack.style.transform = 'translate3d(' + wx.toFixed(1) + 'px,0,0)';
|
||||
const vw = innerWidth;
|
||||
wSpans.forEach((s) => {
|
||||
const r = s.getBoundingClientRect();
|
||||
const d = Math.abs(r.left + r.width / 2 - vw / 2) / (vw * .55);
|
||||
s.style.opacity = (.25 + .75 * clamp(1 - d, 0, 1)).toFixed(3);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- journey: fireworks, counters land in Dylan's stats row ---------- */
|
||||
const jst = $('#jstage'), fw = $('#fw');
|
||||
const FWOK = !!(jst && fw);
|
||||
const fx = FWOK ? fw.getContext('2d') : null;
|
||||
const pinEls = $$('.pn'), jx = $$('.jx'), jfinal = $('#jfinal'), jreplay = $('#jreplay');
|
||||
const cEx = $('#cEx'), cInd = $('#cInd'), cYr = $('#cYr');
|
||||
const PINX = [.12, .31, .5, .69, .88], PINF = [.22, .34, .46, .58, .72], LAUNCH = [200, 950, 1700, 2450, 3300];
|
||||
const COLORS_DARK = ['#ffffff', '#9fe6ff', '#1fb7ff', '#3d8bff', '#d6ecff'];
|
||||
const COLORS_LIGHT = ['#1a7fff', '#0b5fd0', '#1fb7ff', '#5b6cff', '#0870c4'];
|
||||
const isLight = () => document.documentElement.getAttribute('data-theme') === 'light';
|
||||
let FW = { w: 0, h: 0, floor: 0 }, PH = [0, 0, 0, 0, 0], rockets = [], parts = [], extra = [];
|
||||
let fwState = 'idle', fwStart = 0, fwVis = false, cleared = true, fwLaunched = [false, false, false, false, false], fwPlayed = false;
|
||||
const countUp = (el, to, suf) => {
|
||||
if (reduce) { el.textContent = to + (suf || ''); return; }
|
||||
const t0 = performance.now(), d = 1200;
|
||||
(function tick(n) {
|
||||
const k = clamp((n - t0) / d, 0, 1);
|
||||
el.textContent = Math.round(to * (1 - Math.pow(1 - k, 3))) + (k >= 1 ? (suf || '') : '');
|
||||
if (k < 1) requestAnimationFrame(tick);
|
||||
})(t0);
|
||||
};
|
||||
const pop = (el) => { el.classList.remove('pop'); void el.offsetWidth; el.classList.add('pop'); };
|
||||
function sizeFW() {
|
||||
if (!FWOK) return;
|
||||
const r = jst.getBoundingClientRect(), dpr = Math.min(devicePixelRatio || 1, 2);
|
||||
fw.width = Math.round(r.width * dpr); fw.height = Math.round(r.height * dpr);
|
||||
fx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
FW.w = r.width; FW.h = r.height; FW.floor = FW.h * .83;
|
||||
pinEls.forEach((p, i) => { PH[i] = Math.round(FW.h * PINF[i] * .88); p.style.left = (PINX[i] * 100) + '%'; p.style.setProperty('--ph', PH[i] + 'px'); });
|
||||
}
|
||||
function burst(x, y, n, spread) {
|
||||
const COLORS = isLight() ? COLORS_LIGHT : COLORS_DARK;
|
||||
const base = COLORS[(Math.random() * COLORS.length) | 0], sp = spread || 1;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = Math.random() * Math.PI * 2, v = (50 + Math.random() * 190) * sp;
|
||||
parts.push({ x, y, px: x, py: y, vx: Math.cos(a) * v, vy: Math.sin(a) * v, life: 0, max: .85 + Math.random() * .7, c: Math.random() < .65 ? base : COLORS[(Math.random() * COLORS.length) | 0], w: Math.random() < .2 ? 2.2 : 1.3 });
|
||||
}
|
||||
}
|
||||
function launch(i) { pinEls[i].classList.add('rise'); rockets.push({ i, x: PINX[i] * FW.w, y: FW.floor, ty: FW.floor - PH[i], t: 0, dur: .55, trail: [] }); }
|
||||
function arrive(i) {
|
||||
const R = { x: PINX[i] * FW.w, y: FW.floor - PH[i] };
|
||||
pinEls[i].classList.add('done');
|
||||
if (i < 4) {
|
||||
jx[i] && jx[i].classList.add('lit');
|
||||
if (cEx) { cEx.textContent = i + 1; pop(cEx); }
|
||||
burst(R.x, R.y, 72, 1);
|
||||
} else {
|
||||
burst(R.x, R.y, 120, 1.3);
|
||||
extra = [{ t: .12, x: .3, y: .34 }, { t: .3, x: .68, y: .28 }, { t: .5, x: .5, y: .2 }, { t: .7, x: .15, y: .3 }, { t: .9, x: .85, y: .4 }];
|
||||
jfinal.classList.add('show');
|
||||
if (cInd) { countUp(cInd, 6); pop(cInd); }
|
||||
if (cYr) { countUp(cYr, 8, '+'); pop(cYr); }
|
||||
fwState = 'done'; fwPlayed = true; jreplay.classList.add('show');
|
||||
}
|
||||
}
|
||||
function resetFW() {
|
||||
fwState = 'idle'; rockets = []; parts = []; extra = [];
|
||||
pinEls.forEach((p) => p.classList.remove('rise', 'done')); jx.forEach((x) => x.classList.remove('lit'));
|
||||
[cEx, cInd, cYr].forEach((el) => el && (el.textContent = '0'));
|
||||
jfinal.classList.remove('show'); jreplay.classList.remove('show');
|
||||
fx.clearRect(0, 0, FW.w, FW.h);
|
||||
}
|
||||
function startFW() {
|
||||
if (fwState !== 'idle') return;
|
||||
sizeFW();
|
||||
if (reduce) {
|
||||
pinEls.forEach((p) => p.classList.add('rise', 'done')); jx.forEach((x) => x.classList.add('lit'));
|
||||
cEx.textContent = '4'; cInd.textContent = '6'; cYr.textContent = '8+'; jfinal.classList.add('show');
|
||||
fwState = 'done'; fwPlayed = true; return;
|
||||
}
|
||||
[cEx, cInd, cYr].forEach((el) => el && (el.textContent = '0'));
|
||||
fwState = 'play'; fwStart = performance.now(); fwLaunched = [false, false, false, false, false];
|
||||
}
|
||||
if (FWOK) {
|
||||
new IntersectionObserver((es) => {
|
||||
const e = es[0]; fwVis = e.isIntersecting;
|
||||
if (e.intersectionRatio >= .45 && fwState === 'idle' && !fwPlayed) startFW();
|
||||
}, { threshold: [0, .45] }).observe(jst);
|
||||
jreplay.addEventListener('click', () => { resetFW(); fwPlayed = false; startFW(); });
|
||||
addEventListener('resize', sizeFW);
|
||||
sizeFW();
|
||||
}
|
||||
function fwFrame(now, dt) {
|
||||
if (fwState === 'idle' && !parts.length && !rockets.length) { if (!cleared) { fx.clearRect(0, 0, FW.w, FW.h); cleared = true; } return; }
|
||||
cleared = false;
|
||||
if (fwState === 'play') { const el = now - fwStart; LAUNCH.forEach((t, i) => { if (!fwLaunched[i] && el >= t) { fwLaunched[i] = true; launch(i); } }); }
|
||||
if (extra.length) { extra.forEach((e) => { e.t -= dt; }); extra = extra.filter((e) => { if (e.t <= 0) { burst(e.x * FW.w, e.y * FW.h, 70, 1.1); return false; } return true; }); }
|
||||
const light = isLight();
|
||||
fx.clearRect(0, 0, FW.w, FW.h); fx.globalCompositeOperation = light ? 'source-over' : 'lighter'; fx.lineCap = 'round';
|
||||
rockets = rockets.filter((r) => {
|
||||
r.t += dt; const k = Math.min(1, r.t / r.dur), e = 1 - Math.pow(1 - k, 3); r.y = lerp(FW.floor, r.ty, e);
|
||||
r.trail.push({ x: r.x + (Math.random() - .5) * 1.5, y: r.y }); if (r.trail.length > 14) r.trail.shift();
|
||||
for (let j = 1; j < r.trail.length; j++) { const a = j / r.trail.length; fx.strokeStyle = (light ? 'rgba(26,127,255,' : 'rgba(159,230,255,') + (a * .8) + ')'; fx.lineWidth = 2.4 * a; fx.beginPath(); fx.moveTo(r.trail[j - 1].x, r.trail[j - 1].y); fx.lineTo(r.trail[j].x, r.trail[j].y); fx.stroke(); }
|
||||
fx.fillStyle = light ? '#0b5fd0' : '#fff'; fx.beginPath(); fx.arc(r.x, r.y, 2.6, 0, 6.283); fx.fill();
|
||||
if (k >= 1) { arrive(r.i); return false; } return true;
|
||||
});
|
||||
parts = parts.filter((p) => {
|
||||
p.life += dt; if (p.life >= p.max) return false;
|
||||
p.vx *= Math.exp(-1.3 * dt); p.vy = p.vy * Math.exp(-1.3 * dt) + 130 * dt;
|
||||
p.px = p.x; p.py = p.y; p.x += p.vx * dt; p.y += p.vy * dt;
|
||||
fx.globalAlpha = 1 - Math.pow(p.life / p.max, 2); fx.strokeStyle = p.c; fx.lineWidth = p.w;
|
||||
fx.beginPath(); fx.moveTo(p.px, p.py); fx.lineTo(p.x, p.y); fx.stroke();
|
||||
return true;
|
||||
});
|
||||
fx.globalAlpha = 1; fx.globalCompositeOperation = 'source-over';
|
||||
}
|
||||
|
||||
/* ---------- main loop ---------- */
|
||||
let last = performance.now();
|
||||
(function loop(now) {
|
||||
requestAnimationFrame(loop);
|
||||
const dt = Math.min(.05, (now - last) / 1000); last = now;
|
||||
if (wVis && wTrack) marquee(dt);
|
||||
if (FWOK && (fwVis || fwState === 'play' || parts.length || rockets.length)) fwFrame(now, dt);
|
||||
})(last);
|
||||
|
||||
/* ---------- ventures: tabs + a 3D object that matches each business ---------- */
|
||||
const V = [
|
||||
{ n: 'MARKETING & AI SEARCH', d: 'Leading teams through a constantly changing search landscape.', c: '#1fb7ff', m: 'magnifier' },
|
||||
{ n: 'AI SOFTWARE', d: 'Building around new opportunities created by AI.', c: '#7a7bff', m: 'chip' },
|
||||
{ n: 'SPIRITS', d: 'Taking a consumer brand from idea to market.', c: '#3fa9ff', m: 'bottle' },
|
||||
{ n: 'EDUCATION TECHNOLOGY', d: 'Creating technology designed for real classrooms.', c: '#27d9ff', m: 'cap' },
|
||||
{ n: 'CYBERSECURITY', d: 'Solving security challenges for growing businesses.', c: '#8a6bff', m: 'lock' },
|
||||
{ n: 'E-COMMERCE', d: 'Turning product ideas into businesses built around real customers.', c: '#1a8bff', m: 'bag' },
|
||||
];
|
||||
const vtabs = $('#vtabs'), vpanel = $('#vpanel');
|
||||
if (!vtabs || !vpanel) return;
|
||||
const vt = $('#vt'), vname = $('#vname'), vdesc = $('#vdesc'), vnum = $('#vnum');
|
||||
let vi = 0, vSwitch = () => {};
|
||||
V.forEach((v, i) => {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button'; b.setAttribute('role', 'tab'); b.textContent = v.n;
|
||||
b.addEventListener('click', () => setV(i));
|
||||
vtabs.appendChild(b);
|
||||
});
|
||||
const vbtns = $$('button', vtabs);
|
||||
function setV(i, first) {
|
||||
vi = (i + V.length) % V.length;
|
||||
vbtns.forEach((b, n) => b.setAttribute('aria-selected', String(n === vi)));
|
||||
const apply = () => { vname.textContent = V[vi].n; vdesc.textContent = V[vi].d; vnum.textContent = '0' + (vi + 1) + ' / 06'; vpanel.style.setProperty('--ac', V[vi].c); vt.classList.remove('out'); };
|
||||
if (first || reduce) apply(); else { vt.classList.add('out'); setTimeout(apply, 200); }
|
||||
vSwitch(vi);
|
||||
const sel = vbtns[vi];
|
||||
if (!first && sel && vtabs.scrollWidth > vtabs.clientWidth) vtabs.scrollTo({ left: sel.offsetLeft - vtabs.clientWidth / 2 + sel.clientWidth / 2, behavior: 'smooth' });
|
||||
}
|
||||
$('#vprev').addEventListener('click', () => setV(vi - 1));
|
||||
$('#vnext').addEventListener('click', () => setV(vi + 1));
|
||||
vpanel.setAttribute('tabindex', '0');
|
||||
vpanel.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight') setV(vi + 1); if (e.key === 'ArrowLeft') setV(vi - 1); });
|
||||
let sx0 = null;
|
||||
vpanel.addEventListener('touchstart', (e) => { sx0 = e.touches[0].clientX; }, { passive: true });
|
||||
vpanel.addEventListener('touchend', (e) => { if (sx0 === null) return; const dx = e.changedTouches[0].clientX - sx0; if (Math.abs(dx) > 50) setV(vi + (dx < 0 ? 1 : -1)); sx0 = null; });
|
||||
setV(0, true);
|
||||
|
||||
if (typeof THREE === 'undefined') return;
|
||||
(function () {
|
||||
const canvas = $('#vcanvas');
|
||||
let r;
|
||||
try { r = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); } catch (e) { return; }
|
||||
r.setPixelRatio(Math.min(devicePixelRatio || 1, 2));
|
||||
r.outputEncoding = THREE.sRGBEncoding; r.toneMapping = THREE.ACESFilmicToneMapping; r.toneMappingExposure = 1.2;
|
||||
|
||||
// Studio reflections painted on a canvas, as in option 2.
|
||||
const ec = document.createElement('canvas'); ec.width = 1024; ec.height = 512;
|
||||
const e = ec.getContext('2d');
|
||||
const g0 = e.createLinearGradient(0, 0, 0, 512); g0.addColorStop(0, '#1b2f5c'); g0.addColorStop(.5, '#05070f'); g0.addColorStop(1, '#000');
|
||||
e.fillStyle = g0; e.fillRect(0, 0, 1024, 512);
|
||||
const box = (x, y, w, h, c, b) => { e.save(); e.shadowColor = c; e.shadowBlur = b; e.fillStyle = c; e.fillRect(x, y, w, h); e.restore(); };
|
||||
box(120, 50, 300, 36, '#ffffff', 40); box(560, 40, 260, 30, '#d6e8ff', 40); box(250, 150, 26, 240, '#ffffff', 30); box(610, 130, 34, 260, '#2aa8ff', 34); box(860, 140, 30, 240, '#1a7fff', 34); box(40, 160, 26, 220, '#1fb7ff', 30); box(440, 200, 18, 200, '#bfe6ff', 26);
|
||||
const tex = new THREE.CanvasTexture(ec); tex.mapping = THREE.EquirectangularReflectionMapping; tex.encoding = THREE.sRGBEncoding;
|
||||
const env = new THREE.PMREMGenerator(r).fromEquirectangular(tex).texture;
|
||||
|
||||
const scene = new THREE.Scene(), cam = new THREE.PerspectiveCamera(32, 1, .1, 100);
|
||||
cam.position.set(0, 0, 9);
|
||||
const metal = new THREE.MeshStandardMaterial({ color: 0x0b2c6e, metalness: .9, roughness: .22, envMap: env, envMapIntensity: 1.5, emissive: 0x1a7fff, emissiveIntensity: .15 });
|
||||
const chrome = new THREE.MeshStandardMaterial({ color: 0xdfe8f5, metalness: 1, roughness: .12, envMap: env, envMapIntensity: 1.6 });
|
||||
const glass = new THREE.MeshPhysicalMaterial({ color: 0x9fdcff, metalness: .1, roughness: .05, transmission: .6, transparent: true, opacity: .55, envMap: env, envMapIntensity: 1.4 });
|
||||
const glow = new THREE.MeshBasicMaterial({ color: 0x9fe6ff });
|
||||
const holder = new THREE.Group();
|
||||
scene.add(holder);
|
||||
const key = new THREE.PointLight(0x1fb7ff, 60, 30); key.position.set(-4, 3, 6); scene.add(key);
|
||||
scene.add(new THREE.AmbientLight(0x2a4a99, .8));
|
||||
const k2 = new THREE.DirectionalLight(0xffffff, 1.1); k2.position.set(3, 4, 5); scene.add(k2);
|
||||
|
||||
const M = (geo, mat) => new THREE.Mesh(geo, mat);
|
||||
const BUILD = {
|
||||
// Marketing & AI Search: a magnifying glass over rising bars
|
||||
magnifier() {
|
||||
const g = new THREE.Group();
|
||||
const ring = M(new THREE.TorusGeometry(1.05, .17, 24, 64), metal);
|
||||
const lens = M(new THREE.CircleGeometry(1.0, 48), glass);
|
||||
const handle = M(new THREE.CylinderGeometry(.16, .2, 1.6, 24), chrome);
|
||||
handle.position.set(1.15, -1.15, 0); handle.rotation.z = Math.PI / 4;
|
||||
const lensGroup = new THREE.Group(); lensGroup.add(ring, lens, handle); lensGroup.position.set(.25, .35, .35);
|
||||
[[-.75, .45], [-.25, .8], [.25, 1.15]].forEach(([x, h], i) => {
|
||||
const bar = M(new THREE.BoxGeometry(.32, h, .32), i === 2 ? glow : metal);
|
||||
bar.position.set(x - .1, -1.05 + h / 2, -.6); g.add(bar);
|
||||
});
|
||||
g.add(lensGroup);
|
||||
return g;
|
||||
},
|
||||
// AI Software: a processor chip with pins and a glowing core
|
||||
chip() {
|
||||
const g = new THREE.Group();
|
||||
g.add(M(new THREE.BoxGeometry(2.2, 2.2, .32), metal));
|
||||
const core = M(new THREE.BoxGeometry(1.1, 1.1, .1), glow); core.position.z = .2; g.add(core);
|
||||
const pinGeo = new THREE.BoxGeometry(.14, .42, .1);
|
||||
for (let s = 0; s < 4; s++) for (let i = 0; i < 5; i++) {
|
||||
const p = M(pinGeo, chrome); const off = -0.8 + i * .4;
|
||||
if (s === 0) p.position.set(off, 1.3, 0);
|
||||
if (s === 1) p.position.set(off, -1.3, 0);
|
||||
if (s === 2) { p.position.set(1.3, off, 0); p.rotation.z = Math.PI / 2; }
|
||||
if (s === 3) { p.position.set(-1.3, off, 0); p.rotation.z = Math.PI / 2; }
|
||||
g.add(p);
|
||||
}
|
||||
g.rotation.x = -.35;
|
||||
return g;
|
||||
},
|
||||
// Spirits: a bottle (lathe profile) with a chrome cap
|
||||
bottle() {
|
||||
const g = new THREE.Group();
|
||||
const pts = [[0, -1.7], [.78, -1.7], [.86, -1.55], [.86, .35], [.7, .7], [.32, 1.05], [.27, 1.55], [0, 1.55]].map(([x, y]) => new THREE.Vector2(x, y));
|
||||
g.add(M(new THREE.LatheGeometry(pts, 48), glass));
|
||||
const liquid = M(new THREE.CylinderGeometry(.78, .78, 1.6, 48), metal); liquid.position.y = -.85; g.add(liquid);
|
||||
const label = M(new THREE.CylinderGeometry(.875, .875, .55, 48, 1, true), glow); label.position.y = -.35; g.add(label);
|
||||
const cap = M(new THREE.CylinderGeometry(.3, .3, .4, 32), chrome); cap.position.y = 1.72; g.add(cap);
|
||||
g.scale.setScalar(.85);
|
||||
return g;
|
||||
},
|
||||
// Education Technology: a graduation cap
|
||||
cap() {
|
||||
const g = new THREE.Group();
|
||||
const board = M(new THREE.BoxGeometry(2.6, .12, 2.6), metal); board.rotation.y = Math.PI / 4; board.position.y = .45; g.add(board);
|
||||
g.add(M(new THREE.CylinderGeometry(.95, 1.05, .9, 40, 1, true), chrome));
|
||||
const bottom = M(new THREE.CircleGeometry(.95, 40), metal); bottom.rotation.x = Math.PI / 2; bottom.position.y = -.45; g.add(bottom);
|
||||
const button = M(new THREE.SphereGeometry(.12, 16, 16), glow); button.position.y = .55; g.add(button);
|
||||
const cord = M(new THREE.CylinderGeometry(.03, .03, 1.3, 8), glow); cord.position.set(.85, -.1, .85); g.add(cord);
|
||||
const tassel = M(new THREE.ConeGeometry(.12, .4, 16), glow); tassel.position.set(.85, -.85, .85); g.add(tassel);
|
||||
g.rotation.x = .35;
|
||||
return g;
|
||||
},
|
||||
// Cybersecurity: a padlock
|
||||
lock() {
|
||||
const g = new THREE.Group();
|
||||
const body = M(new THREE.BoxGeometry(2, 1.6, .7), metal); body.position.y = -.5; g.add(body);
|
||||
const shackle = M(new THREE.TorusGeometry(.62, .16, 20, 48, Math.PI), chrome); shackle.position.y = .3; g.add(shackle);
|
||||
[-.62, .62].forEach((x) => { const leg = M(new THREE.CylinderGeometry(.16, .16, .5, 20), chrome); leg.position.set(x, .05, 0); g.add(leg); });
|
||||
const hole = M(new THREE.CylinderGeometry(.17, .17, .1, 24), glow); hole.rotation.x = Math.PI / 2; hole.position.set(0, -.35, .36); g.add(hole);
|
||||
const slot = M(new THREE.BoxGeometry(.12, .4, .1), glow); slot.position.set(0, -.62, .36); g.add(slot);
|
||||
return g;
|
||||
},
|
||||
// E-commerce: a shopping bag with handles
|
||||
bag() {
|
||||
const g = new THREE.Group();
|
||||
g.add(M(new THREE.BoxGeometry(2, 2.2, .9), metal));
|
||||
const band = M(new THREE.BoxGeometry(2.02, .28, .92), glow); band.position.y = .6; g.add(band);
|
||||
[-.42, .42].forEach((z) => { const h = M(new THREE.TorusGeometry(.5, .09, 16, 40, Math.PI), chrome); h.position.set(0, 1.1, z * .9); g.add(h); });
|
||||
const tag = M(new THREE.BoxGeometry(.5, .32, .05), chrome); tag.position.set(.55, -.45, .48); tag.rotation.z = -.25; g.add(tag);
|
||||
g.rotation.x = .1; g.scale.setScalar(.9);
|
||||
return g;
|
||||
},
|
||||
};
|
||||
|
||||
let model = null, born = 0;
|
||||
const dispose = (o) => o.traverse((c) => { if (c.geometry) c.geometry.dispose(); });
|
||||
vSwitch = (i) => {
|
||||
if (model) { holder.remove(model); dispose(model); }
|
||||
model = BUILD[V[i].m]();
|
||||
holder.add(model);
|
||||
born = performance.now();
|
||||
key.color.set(V[i].c); metal.emissive.set(V[i].c); glow.color.set(V[i].c).lerp(new THREE.Color(0xffffff), .45);
|
||||
};
|
||||
vSwitch(vi);
|
||||
let vis = true;
|
||||
new IntersectionObserver((es) => { vis = es[0].isIntersecting; }).observe(canvas);
|
||||
const resize = () => { const w = canvas.clientWidth || 1, h = canvas.clientHeight || 1; r.setSize(w, h, false); cam.aspect = w / h; cam.updateProjectionMatrix(); };
|
||||
addEventListener('resize', resize); resize();
|
||||
let mx = 0, my = 0;
|
||||
vpanel.addEventListener('pointermove', (ev) => { const b = vpanel.getBoundingClientRect(); mx = ((ev.clientX - b.left) / b.width - .5) * 2; my = ((ev.clientY - b.top) / b.height - .5) * 2; });
|
||||
let lt = performance.now();
|
||||
(function loop(now) {
|
||||
requestAnimationFrame(loop);
|
||||
if (!vis) return;
|
||||
const dt = Math.min(.05, (now - lt) / 1000); lt = now;
|
||||
const k = reduce ? 1 : clamp((now - born) / 650, 0, 1), sc = reduce ? 1 : (.3 + .7 * (1 - Math.pow(1 - k, 3)));
|
||||
holder.scale.setScalar(sc);
|
||||
if (!reduce) { holder.rotation.y += dt * (.45 + (1 - k) * 6); }
|
||||
holder.rotation.x += ((my * .35) - holder.rotation.x) * .06;
|
||||
holder.position.x += ((mx * .5) - holder.position.x) * .06;
|
||||
r.render(scene, cam);
|
||||
})(lt);
|
||||
})();
|
||||
})();
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 223 KiB |
@@ -0,0 +1,147 @@
|
||||
(function () {
|
||||
window.__site = true;
|
||||
var root = document.documentElement;
|
||||
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
// Theme toggle: remembers the visitor's choice; otherwise follows the system.
|
||||
var themeBtn = document.getElementById('theme-btn');
|
||||
var current = function () { return root.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; };
|
||||
var paint = function () {
|
||||
if (!themeBtn) return;
|
||||
var dark = current() === 'dark';
|
||||
themeBtn.setAttribute('aria-label', dark ? 'Switch to light mode' : 'Switch to dark mode');
|
||||
themeBtn.setAttribute('aria-pressed', String(dark));
|
||||
};
|
||||
var apply = function (t, save) {
|
||||
root.setAttribute('data-theme', t);
|
||||
if (save) { try { localStorage.setItem('theme', t); } catch (e) {} }
|
||||
paint();
|
||||
};
|
||||
paint();
|
||||
if (themeBtn) themeBtn.addEventListener('click', function () { apply(current() === 'dark' ? 'light' : 'dark', true); });
|
||||
|
||||
// Nav border on scroll
|
||||
var nav = document.getElementById('nav');
|
||||
var progress = document.getElementById('progress');
|
||||
var onScroll = function () {
|
||||
nav && nav.classList.toggle('is-scrolled', window.scrollY > 8);
|
||||
if (progress) {
|
||||
var max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight);
|
||||
progress.style.transform = 'scaleX(' + Math.min(1, Math.max(0, window.scrollY / max)) + ')';
|
||||
}
|
||||
};
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
||||
// Mobile menu
|
||||
var menuBtn = document.getElementById('menu-btn');
|
||||
var menu = document.getElementById('mobile-menu');
|
||||
if (menuBtn && menu) {
|
||||
menuBtn.addEventListener('click', function () {
|
||||
var open = menu.hidden;
|
||||
menu.hidden = !open;
|
||||
menuBtn.setAttribute('aria-expanded', String(open));
|
||||
menuBtn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
|
||||
});
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && !menu.hidden) { menu.hidden = true; menuBtn.setAttribute('aria-expanded', 'false'); menuBtn.focus(); }
|
||||
});
|
||||
}
|
||||
|
||||
// Reveal on scroll
|
||||
var reveals = document.querySelectorAll('.reveal');
|
||||
if ('IntersectionObserver' in window && !reduce) {
|
||||
var io = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (en) {
|
||||
if (en.isIntersecting) { en.target.classList.add('is-in'); io.unobserve(en.target); }
|
||||
});
|
||||
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.05 });
|
||||
reveals.forEach(function (el) { io.observe(el); });
|
||||
} else {
|
||||
reveals.forEach(function (el) { el.classList.add('is-in'); });
|
||||
}
|
||||
|
||||
// Count-up numbers: <b data-count="8" data-suffix="+">8+</b>
|
||||
var counters = document.querySelectorAll('[data-count]');
|
||||
var run = function (el) {
|
||||
var end = parseInt(el.getAttribute('data-count'), 10);
|
||||
var suffix = el.getAttribute('data-suffix') || '';
|
||||
var start = null;
|
||||
var dur = 1200;
|
||||
var step = function (t) {
|
||||
if (!start) start = t;
|
||||
var p = Math.min((t - start) / dur, 1);
|
||||
var eased = 1 - Math.pow(1 - p, 3);
|
||||
el.textContent = Math.round(end * eased) + suffix;
|
||||
if (p < 1) requestAnimationFrame(step);
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
};
|
||||
if ('IntersectionObserver' in window && !reduce) {
|
||||
var co = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (en) { if (en.isIntersecting) { run(en.target); co.unobserve(en.target); } });
|
||||
}, { threshold: 0.6 });
|
||||
counters.forEach(function (el) { co.observe(el); });
|
||||
}
|
||||
|
||||
// Tabs (service lines)
|
||||
document.querySelectorAll('[data-tabs]').forEach(function (wrap) {
|
||||
var btns = wrap.querySelectorAll('[role="tab"]');
|
||||
var panels = wrap.querySelectorAll('[role="tabpanel"]');
|
||||
var select = function (i, focus) {
|
||||
btns.forEach(function (b, j) {
|
||||
var on = i === j;
|
||||
b.setAttribute('aria-selected', String(on));
|
||||
b.tabIndex = on ? 0 : -1;
|
||||
panels[j].hidden = !on;
|
||||
});
|
||||
if (focus) btns[i].focus();
|
||||
};
|
||||
btns.forEach(function (b, i) {
|
||||
b.addEventListener('click', function () { select(i); });
|
||||
b.addEventListener('keydown', function (e) {
|
||||
var k = e.key;
|
||||
if (k === 'ArrowDown' || k === 'ArrowRight') { e.preventDefault(); select((i + 1) % btns.length, true); }
|
||||
if (k === 'ArrowUp' || k === 'ArrowLeft') { e.preventDefault(); select((i - 1 + btns.length) % btns.length, true); }
|
||||
});
|
||||
});
|
||||
var hash = location.hash.replace('#', '');
|
||||
var start = 0;
|
||||
panels.forEach(function (p, j) { if (p.id === hash) start = j; });
|
||||
select(start);
|
||||
});
|
||||
|
||||
// Copy buttons
|
||||
document.querySelectorAll('[data-copy]').forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
var text = b.getAttribute('data-copy');
|
||||
var label = b.textContent;
|
||||
var done = function () { b.textContent = 'Copied'; setTimeout(function () { b.textContent = label; }, 1600); };
|
||||
try { navigator.clipboard.writeText(text).then(done, function () {}); } catch (e) {}
|
||||
});
|
||||
});
|
||||
|
||||
// Inquiry form: opens the visitor's email client with the message prefilled.
|
||||
var form = document.getElementById('inquiry');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
if (!form.reportValidity()) return;
|
||||
var d = new FormData(form);
|
||||
var body = [
|
||||
'Name: ' + d.get('name'),
|
||||
'Email: ' + d.get('email'),
|
||||
d.get('company') ? 'Company: ' + d.get('company') : '',
|
||||
d.get('topic') ? 'Topic: ' + d.get('topic') : '',
|
||||
'',
|
||||
d.get('message')
|
||||
].filter(function (l, i) { return l !== '' || i === 4; }).join('\n');
|
||||
var subject = 'Inquiry from ' + d.get('name') + (d.get('topic') ? ' — ' + d.get('topic') : '');
|
||||
window.location.href = 'mailto:' + form.getAttribute('data-to') + '?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body);
|
||||
var status = document.getElementById('form-status');
|
||||
if (status) status.textContent = 'Your email app should open with the message ready to send.';
|
||||
});
|
||||
}
|
||||
|
||||
root.classList.add('ready');
|
||||
})();
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="16" fill="#0b0b0d"/><g transform="translate(6 15) scale(.5)"><path fill="#fff" fill-rule="evenodd" d="M9 4h55c10 0 13 7 9 14L58 46c-3 6-8 8-15 8.5L3 57zM21.5 15.5 18.3 45.8 40 44.5c4-.3 6-1.5 7.5-4.3l12-21.4c1.2-2.2.3-3.3-2-3.3z"/><path fill="#33b9ff" d="M24 59 102 1 91 52 72 54.5 79 33z"/><path fill="#8fdcff" d="M24 59 102 1 79 33z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 441 B |
@@ -0,0 +1,93 @@
|
||||
Copyright 2022 The Inter Project Authors (https://github.com/rsms/inter-tight) InterTight-Italic[wght].ttf: Copyright 2022 The Inter Project Authors (https://github.com/rsms/inter-tight)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright © 2010-2021 three.js authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||