diff --git a/client/portal/public/cursors/cp-arrow.png b/client/portal/public/cursors/cp-arrow.png new file mode 100644 index 000000000..56915af35 Binary files /dev/null and b/client/portal/public/cursors/cp-arrow.png differ diff --git a/client/portal/public/cursors/cp-arrow@2x.png b/client/portal/public/cursors/cp-arrow@2x.png new file mode 100644 index 000000000..589a94c6a Binary files /dev/null and b/client/portal/public/cursors/cp-arrow@2x.png differ diff --git a/client/portal/public/cursors/cp-beam.png b/client/portal/public/cursors/cp-beam.png new file mode 100644 index 000000000..10781eb5b Binary files /dev/null and b/client/portal/public/cursors/cp-beam.png differ diff --git a/client/portal/public/cursors/cp-beam@2x.png b/client/portal/public/cursors/cp-beam@2x.png new file mode 100644 index 000000000..25e0ee518 Binary files /dev/null and b/client/portal/public/cursors/cp-beam@2x.png differ diff --git a/client/portal/public/cursors/cp-link.png b/client/portal/public/cursors/cp-link.png new file mode 100644 index 000000000..b2bc01c1f Binary files /dev/null and b/client/portal/public/cursors/cp-link.png differ diff --git a/client/portal/public/cursors/cp-link@2x.png b/client/portal/public/cursors/cp-link@2x.png new file mode 100644 index 000000000..0db67debf Binary files /dev/null and b/client/portal/public/cursors/cp-link@2x.png differ diff --git a/client/portal/src/assets/fonts/Satoshi-Variable.woff2 b/client/portal/src/assets/fonts/Satoshi-Variable.woff2 new file mode 100644 index 000000000..b00e833ed Binary files /dev/null and b/client/portal/src/assets/fonts/Satoshi-Variable.woff2 differ diff --git a/client/portal/src/assets/fonts/hypik-LICENSE.txt b/client/portal/src/assets/fonts/hypik-LICENSE.txt new file mode 100644 index 000000000..ea5b97a0d --- /dev/null +++ b/client/portal/src/assets/fonts/hypik-LICENSE.txt @@ -0,0 +1,42 @@ + __ _ __ + / /_ __ ______ (_) /__ + / __ \/ / / / __ \/ / //_/ + / / / / /_/ / /_/ / / ,< +/_/ /_/\__, / .___/_/_/|_| + /____/_/ + +———————————————————————————— + +Thanks for downloading HYPIK™ + +hypik.otf / Version 1.0 / January 2022 + + + +ABOUT THIS FONT +——————————————— + +This font was inspired by Roadam by HVNTER, the Vector Heart design movement, and Björk's logo. Apologies that there are no numbers or punctuation. Want to flesh out or improve this font? Feel free! Please see the included Adobe Illustrator file for information on how it was created. Feedback and general comments are welcomed. Visit mattcolewilson.com for more. + + + +TERMS OF USE +———————————— + +This font is completely free for both personal and commercial use. The distribution of this font for financial gain or profit is not permitted under any circumstances. Free distribution is allowed, if you link back to my site: https://mattcolewilson.com/resources + +If you have any questions or comments, please email me! + + + +CONTACT INFORMATION +——————————————————— + +Email: mattcolewilson@gmail.com + +Web: https://mattcolewilson.com + +Twitter: http://twitter.com/mattcolewilson + +Dribbble: http://dribbble.com/mattcolewilson + diff --git a/client/portal/src/assets/fonts/hypik.otf b/client/portal/src/assets/fonts/hypik.otf new file mode 100644 index 000000000..c3fd0c15b Binary files /dev/null and b/client/portal/src/assets/fonts/hypik.otf differ diff --git a/client/portal/src/assets/mascots.webp b/client/portal/src/assets/mascots.webp new file mode 100644 index 000000000..f74f1540a Binary files /dev/null and b/client/portal/src/assets/mascots.webp differ diff --git a/client/portal/src/assets/sky.webp b/client/portal/src/assets/sky.webp new file mode 100644 index 000000000..cb88cd2ea Binary files /dev/null and b/client/portal/src/assets/sky.webp differ diff --git a/client/portal/src/assets/zero-day-wordmark.webp b/client/portal/src/assets/zero-day-wordmark.webp new file mode 100644 index 000000000..7eef6dbfc Binary files /dev/null and b/client/portal/src/assets/zero-day-wordmark.webp differ diff --git a/client/portal/src/index.css b/client/portal/src/index.css index f29fdfa97..486917f1b 100644 --- a/client/portal/src/index.css +++ b/client/portal/src/index.css @@ -5,9 +5,28 @@ restyle the portal without conflicting with upstream on every merge. */ @import "../branding/theme.css"; +/* Zero Day type, from the marketing site: Satoshi for text, Hypik for display. + Hypik is letters only - no digits or punctuation - and its licence asks for + a link back (assets/fonts/hypik-LICENSE.txt). */ +@font-face { + font-family: "Satoshi"; + src: url("./assets/fonts/Satoshi-Variable.woff2") format("woff2"); + font-weight: 300 900; + font-display: swap; +} + +@font-face { + font-family: "Hypik"; + src: url("./assets/fonts/hypik.otf") format("opentype"); + font-display: swap; +} + @custom-variant dark (&:is(.dark *)); @theme inline { + --font-satoshi: "Satoshi", ui-sans-serif, system-ui, sans-serif; + --font-hypik: "Hypik", "Satoshi", ui-sans-serif, sans-serif; + --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); @@ -572,18 +591,6 @@ opacity: 0.4; } -.zero-login-title { - filter: drop-shadow(10px 0 18px rgb(246 43 232 / 14%)) - drop-shadow(-8px 0 18px rgb(33 255 240 / 10%)); -} - -.zero-login-hand { - filter: drop-shadow(18px 0 30px rgb(246 43 232 / 18%)) - drop-shadow(-16px 0 30px rgb(33 255 240 / 16%)); - animation: zero-hand-drift 10s ease-in-out infinite; - will-change: transform; -} - .zero-login-panel { background: linear-gradient( 135deg, @@ -633,8 +640,7 @@ ); } -.zero-cut-sm, -.zero-login-input { +.zero-cut-sm { border-radius: 0; clip-path: polygon( 0 0, @@ -689,29 +695,57 @@ transform: scaleX(1); } -.zero-harp-link { - transition: - filter 240ms ease, - transform 240ms ease; +/* Sign-in page footer links: the same sweep-in underline and nudge as main's + cyan versions, recoloured to the page's grey-and-white palette. */ +.login-sweep-link { + position: relative; + display: inline-block; + text-decoration: none; + transition: color 280ms ease; +} + +.login-sweep-link::before, +.login-sweep-link::after { + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 1px; + content: ""; +} + +.login-sweep-link::before { + background: currentColor; + opacity: 0.5; } -.zero-harp-link span, -.zero-harp-link svg { - transition: color 240ms ease; +.login-sweep-link::after { + background: #fff; + transform: scaleX(0); + transform-origin: left; + transition: transform 280ms ease; } -.zero-harp-link:hover, -.zero-harp-link:focus-visible { - filter: drop-shadow(0 0 8px rgb(33 255 240 / 55%)); +.login-sweep-link:hover, +.login-sweep-link:focus-visible { + color: #fff; outline: none; - transform: translateX(2px); } -.zero-harp-link:hover span, -.zero-harp-link:focus-visible span, -.zero-harp-link:hover svg, -.zero-harp-link:focus-visible svg { - color: #21fff0; +.login-sweep-link:hover::after, +.login-sweep-link:focus-visible::after { + transform: scaleX(1); +} + +.login-harp-link { + transition: + color 240ms ease, + transform 240ms ease; +} + +.login-harp-link:hover, +.login-harp-link:focus-visible { + transform: translateX(2px); } /* Zero Day hacker portal. The theme class lives at the shared hacker layout, @@ -990,16 +1024,6 @@ body.hacker-zero-portals } } -@keyframes zero-hand-drift { - 0%, - 100% { - transform: translate3d(0, 0, 0) rotate(-1deg); - } - 50% { - transform: translate3d(-8px, -10px, 0) rotate(1deg); - } -} - @media (max-width: 639px) { .zero-login { background-image: @@ -1013,18 +1037,216 @@ body.hacker-zero-portals } } -/* Below `lg` the sign-in screen is one screenful: the title pins to the top and - the access panel takes whatever height is left, with the mascot hand sitting - behind it. On a short viewport (a small phone, or one in landscape) the - supporting line steps aside rather than pushing the form off-screen. */ -@media (max-width: 1023px) and (max-height: 720px) { - .zero-login-blurb { - display: none; +/* --------------------------------------------------------------------------- + * Login card border + * + * A conic gradient turns slowly behind the card: `::before` is the 1px rim, + * clipped to the card's notch (`--notch`), and `::after` is the same gradient + * blurred into a soft glow, left unclipped so it can bleed past the corners. + * The card sits on top and must be opaque, or the rim's fill shows through. + * `@property` lets the angle itself animate, so only the light moves. + * ------------------------------------------------------------------------ */ + +@property --login-angle { + syntax: ""; + initial-value: 200deg; + inherits: false; +} + +@keyframes login-border-turn { + to { + --login-angle: 560deg; } } +.login-glow { + position: relative; + isolation: isolate; +} + +.login-glow::before, +.login-glow::after { + position: absolute; + inset: -1px; + background: conic-gradient( + from var(--login-angle), + #7828ff, + transparent 22%, + transparent 48%, + #dbc4ff 58%, + transparent 70%, + #7828ff + ); + content: ""; + animation: login-border-turn 12s linear infinite; +} + +.login-glow::before { + z-index: -1; + clip-path: var(--notch); + background-color: rgb(255 255 255 / 8%); +} + +.login-glow::after { + z-index: -2; + filter: blur(28px); + opacity: 0.35; +} + +.login-glow-card { + background: #0e011f; + clip-path: var(--notch); +} + @media (prefers-reduced-motion: reduce) { - .zero-login-hand { + .login-glow::before, + .login-glow::after { animation: none; } } + +/* --------------------------------------------------------------------------- + * Shooting stars, from zeroday.hackutd.co (`.star-field` in its globals.css). + * + * The same streak and timing: each star's flight is the first fifth of a long + * cycle and the rest is empty sky. The site restarts its single star from a + * scroll handler; here there is nothing to sync with, so two stars simply loop + * half a cycle apart, one in each direction. Two animations compose on the + * star, one growing and shrinking the tail and one carrying it across; the + * field's tilt angles the path. + * ------------------------------------------------------------------------ */ + +.star-field { + --star-cycle: 14s; + position: absolute; + inset: -20% -15%; + transform: rotate(8deg); + pointer-events: none; +} + +/* A mirrored field: its stars fly right to left, still descending, and the + flip keeps the bright head at the leading end. */ +.star-field-reverse { + transform: scaleX(-1) rotate(8deg); +} + +.shooting-star { + position: absolute; + top: var(--star-top, 26%); + left: 0; + height: 2px; + border-radius: 999px; + /* Bright at the right-hand end, because that end leads. */ + background: linear-gradient( + 90deg, + rgba(219, 196, 255, 0) 0%, + rgba(219, 196, 255, 0.85) 65%, + #ffffff 100% + ); + filter: drop-shadow(0 0 6px rgba(190, 150, 255, 0.95)); + animation: + star-tail var(--star-cycle) ease-in-out infinite, + star-shoot var(--star-cycle) ease-in-out infinite; + animation-delay: var(--star-delay, 0s); +} + +/* The cross-glint at the head, mirrored to make an X. */ +.shooting-star::before, +.shooting-star::after { + content: ""; + position: absolute; + top: calc(50% - 1px); + right: 0; + height: 2px; + border-radius: 100%; + background: linear-gradient( + -45deg, + rgba(219, 196, 255, 0), + rgba(233, 220, 255, 1), + rgba(219, 196, 255, 0) + ); + animation: star-glint var(--star-cycle) ease-in-out infinite; + animation-delay: var(--star-delay, 0s); +} + +.shooting-star::before { + transform: translateX(50%) rotateZ(45deg); +} + +.shooting-star::after { + transform: translateX(50%) rotateZ(-45deg); +} + +@keyframes star-shoot { + 0% { + transform: translateX(0); + } + 20%, + 100% { + transform: translateX(130vw); + } +} + +@keyframes star-tail { + 0% { + width: 0; + } + 4%, + 17% { + width: 180px; + } + 20%, + 100% { + width: 0; + } +} + +@keyframes star-glint { + 0%, + 20%, + 100% { + width: 0; + } + 5%, + 16% { + width: 30px; + } +} + +@media (prefers-reduced-motion: reduce) { + .star-field { + display: none; + } +} + +/* --------------------------------------------------------------------------- + * Cyberpunk cursors, from zeroday.hackutd.co (public/cursors). Scoped to the + * pages that opt in with `.zero-cursor`. The hotspots are the ones baked into + * the original .cur files - the arrow's tip is 8px down its bitmap, the hand's + * at (6,5), the beam's centred at (17,15) - so clicks land where the cursor + * points. Each keeps its native keyword as the fallback. + * ------------------------------------------------------------------------ */ + +.zero-cursor { + cursor: + image-set( + url("/cursors/cp-arrow.png") 1x, + url("/cursors/cp-arrow@2x.png") 2x + ) + 0 8, + auto; +} + +.zero-cursor :is(a, button:not(:disabled), summary, label, [role="button"]) { + cursor: + image-set(url("/cursors/cp-link.png") 1x, url("/cursors/cp-link@2x.png") 2x) + 6 5, + pointer; +} + +.zero-cursor :is(input, textarea, [contenteditable="true"]) { + cursor: + image-set(url("/cursors/cp-beam.png") 1x, url("/cursors/cp-beam@2x.png") 2x) + 17 15, + text; +} diff --git a/client/portal/src/pages/public/LoginPage.tsx b/client/portal/src/pages/public/LoginPage.tsx index 846cad032..4ea505c62 100644 --- a/client/portal/src/pages/public/LoginPage.tsx +++ b/client/portal/src/pages/public/LoginPage.tsx @@ -1,145 +1,128 @@ import { ArrowLeft, ArrowRight, ArrowUpRight, Mail } from "lucide-react"; -import { type ReactNode, useEffect, useState } from "react"; +import { type CSSProperties, type ReactNode, useEffect, useState } from "react"; import { Navigate } from "react-router"; import { createCode } from "supertokens-auth-react/recipe/passwordless"; import { useSessionContext } from "supertokens-auth-react/recipe/session"; import { redirectToThirdPartyLogin } from "supertokens-auth-react/recipe/thirdparty"; import googleIcon from "@/assets/google_icon.webp"; -import logoGlitch from "@/assets/logo_glitch.webp"; -import zeroDayTitle from "@/assets/title-login.webp"; -import { Alert, AlertDescription } from "@/components/ui/alert"; +import mascots from "@/assets/mascots.webp"; +import sky from "@/assets/sky.webp"; +import wordmark from "@/assets/zero-day-wordmark.webp"; import { Button } from "@/components/ui/button"; -import { isGoogleAuthEnabled } from "@/shared/auth"; import { checkEmailAuthMethod } from "@/shared/lib/api"; import { fetchLegalConfig } from "./api"; -import MascotField from "./components/MascotField"; import type { LegalConfig } from "./types"; type LoginState = "email" | "sending" | "sent" | "error"; +// The marketing site's chamfer: top-left and bottom-right corners cut, in px so +// the corners stay square at any width. A clip-path also clips focus rings, so +// buttons draw focus as an inset shadow instead. +const NOTCH = + "polygon(12px 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%, 0 12px)"; +const NOTCH_SM = + "polygon(8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%, 0 8px)"; +const NOTCH_LG = + "polygon(20px 0, 100% 0, 100% calc(100% - 20px), calc(100% - 20px) 100%, 0 100%, 0 20px)"; + +const BUTTON = + "h-14 w-full rounded-none text-[13px] font-medium tracking-[0.1em] uppercase focus-visible:ring-0 focus-visible:shadow-[inset_0_0_0_2px_#fff]"; + +const LEGAL_LINK = "login-sweep-link text-[#8b93a1]"; + +// The night sky from zeroday.hackutd.co behind a single card. The art is +// portrait and fades to black below the stars, so it is pinned to the top and +// the page's own black carries on underneath at any height. The art is +// mirrored so the moon sits on the left, and from `lg` the card moves right to +// leave the moon and the brightest clouds in view. function ZeroDayShell({ children }: { children: ReactNode }) { return ( -
-
+ -
- - -
- - - - HackUTD // Secure portal - - - MMXXVI +
+ +
+
+
-
-
- HackUTD Zero Day - -
-
-

- System entry // 2026 -

-

- Your gateway to applications, event access, schedules, and - everything HackUTD 2026. -

-
-
- -
-
- - -
- -
-
+ + + + +
+ HackUTD's Zero Day + +
+
+ {/* The mascots peek up from the card's bottom-right corner, cut off + by its edge. Luminosity blending recolours them in the card's + violet, and the mask fades them out before they reach the form. + Only from `lg`, where the card is taller than its content and + leaves them room. */} + {children}
-
-
- Identity verification node 01 -
- {children} -
- -
-
-
+
-
- HACKUTD // ZERO DAY + + Powered by HARP + +
); } -function AccessPanel({ children }: { children: ReactNode }) { +function Eyebrow({ children }: { children: ReactNode }) { return ( -
-
- {children} -
-
+

+ {children} +

); } -function PoweredByHarpLink() { +// Hypik has no digits or punctuation, so headings passed here must be letters +// and spaces only. It runs about 10px wide per px of size in caps, so the size +// tracks the viewport to keep "Enter Zero Day" on one line inside the card. +function Heading({ children }: { children: ReactNode }) { return ( - - Powered by - HARP - - +

+ {children} +

); } @@ -203,20 +186,9 @@ export default function Login() { } }; + // No pre-check against the typed email: it need not match the Google account + // the user picks. The backend rejects a mismatch using Google's own email. const handleGoogleLogin = async () => { - if (email) { - const checkRes = await checkEmailAuthMethod(email); - if (checkRes.status === 200 && checkRes.data?.exists) { - if (checkRes.data.auth_method === "passwordless") { - setState("error"); - setError( - "This email is registered with magic link sign-in. Please use the email option instead.", - ); - return; - } - } - } - try { await redirectToThirdPartyLogin({ thirdPartyId: "google" }); } catch (err) { @@ -236,41 +208,25 @@ export default function Login() { if (state === "sent") { return ( - -
-

- Link dispatched -

- -
- -
- - - -

- Check your inbox. -

-

- We sent a secure sign-in link to - - {email} - -

-
- -
- Open the link within 15 minutes to enter the hacker portal. -
+ +
+ Link dispatched + Check your inbox +

+ We sent a secure sign-in link to{" "} + {email}. + Open it within 15 minutes to enter the hacker portal. +

+
- -
+
); } @@ -278,139 +234,104 @@ export default function Login() { // Email input form return ( - -
-

- Hacker access // 01 -

-
- - - -
-
- -
-

- Authentication required -

-

- Enter Zero Day. -

-

- Sign in or create your hacker account with a secure magic link. -

-
- -
- {state === "error" && error && ( - - - {error} - - - )} - - {/* Magic Link Form */} -
-
- -
- - setEmail(e.target.value)} - required - disabled={state === "sending"} - className="zero-login-input block h-11 w-full sm:h-12 border border-white/15 bg-white/[0.035] pr-4 pl-11 text-sm text-white transition-colors placeholder:text-white/25 hover:border-white/30 focus:border-[#21FFF0]/80 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50" - /> -
-
- -
- - {/* Google OAuth Button (only if enabled) */} - {isGoogleAuthEnabled && ( - <> -
- - - alternate route - - -
- - - - )} - - {(legal?.terms_url || legal?.privacy_policy_url) && ( -

- By continuing, you agree to our{" "} - {legal.terms_url && ( - - Terms of Service - - )} - {legal.terms_url && legal.privacy_policy_url && " and "} - {legal.privacy_policy_url && ( - - Privacy Policy - - )} -

+ Hacker access + + Enter Zero Day + +

+ Sign in or create your hacker account with a secure magic link. +

+ + {state === "error" && error && ( +

+ {error} +

+ )} + +
+ + setEmail(e.target.value)} + required + disabled={state === "sending"} + className="mt-2 block h-14 w-full border-b border-white/15 bg-transparent text-[16px] text-white caret-[#dbc4ff] transition-colors placeholder:text-[#5a6270] hover:border-white/30 focus:border-[#7828ff] focus:outline-none disabled:cursor-not-allowed disabled:opacity-50" + /> +
-
+ + + + {/* TODO: UI preview. The Google option is always shown for now. Before + shipping, restore the `isGoogleAuthEnabled` gate from + "@/shared/auth" (set by VITE_GOOGLE_AUTH_ENABLED=true in + client/portal/.env), or the button errors on click wherever Google + sign-in is not configured. */} +
+ + alternate route + +
+ + + {/* TODO: UI preview. The legal line is always shown for now, with dead + links until the Terms and Privacy Policy URLs are saved in the super + admin settings. Before shipping, restore the gate that hides it (and + each link) when no URL is configured. */} +

+ By continuing, you agree to our{" "} + + Terms of Service + {" "} + and{" "} + + Privacy Policy + + . +

); }