From 213d83ab4260cdd9cd44eeced1fda36444c21c81 Mon Sep 17 00:00:00 2001
From: Takudzwa-Mlambo <157640942+Cod3Uchiha@users.noreply.github.com>
Date: Sun, 9 Aug 2026 07:53:56 +0200
Subject: [PATCH 1/4] Add Supabase client
---
src/supabase.js | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 src/supabase.js
diff --git a/src/supabase.js b/src/supabase.js
new file mode 100644
index 0000000..337b18f
--- /dev/null
+++ b/src/supabase.js
@@ -0,0 +1,6 @@
+import { createClient } from '@supabase/supabase-js'
+
+const SUPABASE_URL = 'https://xevezwgeljeyorkkumhu.supabase.co'
+const SUPABASE_PUBLISHABLE_KEY = 'sb_publishable_kvMIifHrNIQEI9FJshT1fA_TYFzV__p'
+
+export const supabase = createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY)
From bda73b63d5088c84661a8d78fa6d70c39605f54f Mon Sep 17 00:00:00 2001
From: Takudzwa-Mlambo <157640942+Cod3Uchiha@users.noreply.github.com>
Date: Sun, 9 Aug 2026 07:54:03 +0200
Subject: [PATCH 2/4] Add Supabase auth dependency
---
package.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/package.json b/package.json
index ffe6e6b..7d75976 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "@supabase/supabase-js": "2.110.9",
"react": "19.1.1",
"react-dom": "19.1.1"
},
From 66b6e32e80674cf1a6ca7a26f1b1953e4cf2ea2c Mon Sep 17 00:00:00 2001
From: Takudzwa-Mlambo <157640942+Cod3Uchiha@users.noreply.github.com>
Date: Sun, 9 Aug 2026 07:55:23 +0200
Subject: [PATCH 3/4] Add login user dashboard and admin dashboard
---
src/App.jsx | 448 ++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 397 insertions(+), 51 deletions(-)
diff --git a/src/App.jsx b/src/App.jsx
index 3992cd6..4595eb8 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,92 +1,438 @@
-import { useEffect, useRef } from 'react'
+import { useEffect, useRef, useState } from 'react'
+import { supabase } from './supabase.js'
+
+const ADMIN_EMAIL = 'dev@cod3uchiha.com'
+const API_URL = 'https://api.cod3uchiha.com'
+
+function useHashRoute() {
+ const read = () => window.location.hash.replace(/^#\/?/, '') || 'home'
+ const [route, setRoute] = useState(read)
+
+ useEffect(() => {
+ const onHashChange = () => setRoute(read())
+ window.addEventListener('hashchange', onHashChange)
+ return () => window.removeEventListener('hashchange', onHashChange)
+ }, [])
+
+ return route
+}
+
+function AuthModal({ open, onClose }) {
+ const [mode, setMode] = useState('signin')
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [message, setMessage] = useState('')
+ const [error, setError] = useState('')
+
+ useEffect(() => {
+ if (!open) {
+ setPassword('')
+ setMessage('')
+ setError('')
+ }
+ }, [open])
+
+ if (!open) return null
+
+ async function submit(event) {
+ event.preventDefault()
+ setLoading(true)
+ setMessage('')
+ setError('')
+
+ try {
+ if (mode === 'signup') {
+ const { data, error: signUpError } = await supabase.auth.signUp({ email, password })
+ if (signUpError) throw signUpError
+ if (data.session) {
+ onClose()
+ window.location.hash = 'dashboard'
+ } else {
+ setMessage('Account created. Check your email to confirm it, then sign in.')
+ }
+ } else {
+ const { error: signInError } = await supabase.auth.signInWithPassword({ email, password })
+ if (signInError) throw signInError
+ onClose()
+ window.location.hash = 'dashboard'
+ }
+ } catch (authError) {
+ setError(authError.message || 'Authentication failed.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+ Cod3Uchiha account
+
{mode === 'signin' ? 'Sign in' : 'Create account'}
+
+
×
+
+
+
+
+ setMode(mode === 'signin' ? 'signup' : 'signin')}>
+ {mode === 'signin' ? 'Need an account? Sign up' : 'Already have an account? Sign in'}
+
+
+
+ )
+}
function Story() {
return (
The Uchiha Clan and Coding
-
- The Uchiha Clan is renowned for their exceptional abilities and Sharingan eye technique. In the world of coding, we strive to develop our own unique skills and techniques, just like the Uchiha Clan.
-
-
- Programming and developing, like the Sharingan, allow us to perceive and understand the intricacies of technology. With each line of code we write, we unlock new possibilities and shape the digital world around us.
-
-
- As the Uchiha Clan sought to protect and advance their abilities, we, as developers, aim to use our coding skills to create innovative solutions, build amazing projects, and contribute to the ever-evolving field of technology.
-
+ The Uchiha Clan is renowned for their exceptional abilities and Sharingan eye technique. In the world of coding, we strive to develop our own unique skills and techniques, just like the Uchiha Clan.
+ Programming and developing, like the Sharingan, allow us to perceive and understand the intricacies of technology. With each line of code we write, we unlock new possibilities and shape the digital world around us.
+ As the Uchiha Clan sought to protect and advance their abilities, we, as developers, aim to use our coding skills to create innovative solutions, build amazing projects, and contribute to the ever-evolving field of technology.
)
}
function AudioControls() {
const audioRef = useRef(null)
+ return (
+
+
+
audioRef.current?.play()}>Play
+
{
+ if (!audioRef.current) return
+ audioRef.current.pause()
+ audioRef.current.currentTime = 0
+ }}>Stop
+
+ )
+}
+
+function Home({ session, openAuth }) {
+ return (
+
+
+
+
Developer · APIs · Open source
+
Code Uchiha
+
Build with the Cod3Uchiha API. Accounts include 15 successful API requests every day across the entire endpoint catalog.
+
+
Browsing the site and endpoint catalog stays public. Login is only needed for account features and API usage.
+
+
+
+
+
+
+
+ 15 successful requests / day
+ All endpoints share one quota
+ 0 failed requests counted
+
- const play = () => {
- audioRef.current?.play()
+
+
+
+
+
+ )
+}
+
+function Dashboard({ session, openAuth }) {
+ const [data, setData] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState('')
+ const [keyName, setKeyName] = useState('Default')
+ const [newKey, setNewKey] = useState('')
+ const [working, setWorking] = useState(false)
+
+ async function loadDashboard() {
+ if (!session) return
+ setLoading(true)
+ setError('')
+ try {
+ const today = new Date().toISOString().slice(0, 10)
+ const [profileResult, planResult, usageResult, keysResult, endpointResult] = await Promise.all([
+ supabase.from('profiles').select('display_name, plan_id, is_banned, total_requests, created_at').single(),
+ supabase.from('plans').select('id, name, requests_per_day, requests_per_minute, max_api_keys').eq('id', 'free').single(),
+ supabase.from('usage_daily').select('request_count, last_request_at').eq('usage_date', today).maybeSingle(),
+ supabase.from('api_keys').select('id, name, key_prefix, created_at, last_used_at, revoked_at, total_requests').order('created_at', { ascending: false }),
+ supabase.from('usage_endpoint_daily').select('endpoint, request_count').eq('usage_date', today).order('request_count', { ascending: false }).limit(12)
+ ])
+
+ const firstError = [profileResult, planResult, usageResult, keysResult, endpointResult].find((result) => result.error)?.error
+ if (firstError) throw firstError
+
+ setData({
+ profile: profileResult.data,
+ plan: planResult.data,
+ usage: usageResult.data,
+ keys: keysResult.data || [],
+ endpoints: endpointResult.data || []
+ })
+ } catch (loadError) {
+ setError(loadError.message || 'Unable to load dashboard.')
+ } finally {
+ setLoading(false)
+ }
}
- const stop = () => {
- const audio = audioRef.current
- if (!audio) return
+ useEffect(() => {
+ void loadDashboard()
+ }, [session?.user?.id])
+
+ async function createKey() {
+ setWorking(true)
+ setNewKey('')
+ setError('')
+ try {
+ const { data: created, error: createError } = await supabase.rpc('create_api_key', { p_name: keyName || 'Default' })
+ if (createError) throw createError
+ setNewKey(created?.api_key || '')
+ await loadDashboard()
+ } catch (createError) {
+ setError(createError.message || 'Unable to create API key.')
+ } finally {
+ setWorking(false)
+ }
+ }
- audio.pause()
- audio.currentTime = 0
+ async function revokeKey(id) {
+ setWorking(true)
+ try {
+ const { error: revokeError } = await supabase.rpc('revoke_api_key', { p_key_id: id })
+ if (revokeError) throw revokeError
+ await loadDashboard()
+ } catch (revokeError) {
+ setError(revokeError.message || 'Unable to revoke API key.')
+ } finally {
+ setWorking(false)
+ }
}
+ if (!session) {
+ return (
+
+ Dashboard
+ Sign in to view your API quota and manage API keys.
+ Sign in
+
+ )
+ }
+
+ if (loading) return
+ if (!data) return {error || 'Dashboard unavailable.'}
+
+ const used = data.usage?.request_count || 0
+ const limit = data.plan?.requests_per_day || 15
+ const remaining = Math.max(0, limit - used)
+ const activeKeys = data.keys.filter((key) => !key.revoked_at)
+
return (
-
-
-
Play
-
Stop
+
+
+
+ {error &&
{error}
}
+
+
+ Today {used} / {limit} successful requests
+ Remaining {remaining} resets daily
+ All time {data.profile?.total_requests || 0} successful requests
+ API keys {activeKeys.length} / {data.plan?.max_api_keys || 3} active keys
+
+
+
+
+
Credentials
API keys
+
+ setKeyName(event.target.value)} maxLength={40} aria-label="API key name" />
+ Create key
+
+
+
+ {newKey && (
+
+ Copy this key now. It is only shown once.
+ {newKey}
+ navigator.clipboard?.writeText(newKey)}>Copy
+
+ )}
+
+
+ {data.keys.length === 0 &&
No API keys yet.
}
+ {data.keys.map((key) => (
+
+
{key.name} {key.key_prefix}••••••••
+
{key.revoked_at ? 'Revoked' : `${key.total_requests || 0} requests`}
+ {!key.revoked_at &&
revokeKey(key.id)}>Revoke }
+
+ ))}
+
+
+
+
+
+
+ {data.endpoints.length === 0 &&
No successful requests yet today.
}
+ {data.endpoints.map((item) => (
+
{item.endpoint}{item.request_count}
+ ))}
+
+
)
}
-function ContactDetails() {
+function AdminDashboard({ session }) {
+ const [data, setData] = useState(null)
+ const [error, setError] = useState('')
+ const isAdmin = session?.user?.email?.toLowerCase() === ADMIN_EMAIL
+
+ useEffect(() => {
+ if (!isAdmin) return
+ supabase.rpc('get_admin_dashboard').then(({ data: adminData, error: adminError }) => {
+ if (adminError) setError(adminError.message)
+ else setData(adminData)
+ })
+ }, [isAdmin])
+
+ if (!session) return
Admin Sign in with the administrator account.
+ if (!isAdmin) return
403 This dashboard is restricted to {ADMIN_EMAIL}.
+ if (error) return
+ if (!data) return
+
return (
-
+
+
Restricted Admin dashboard {ADMIN_EMAIL}
+
+ Users {data.summary?.users || 0}
+ Requests today {data.summary?.requests_today || 0}
+ Total requests {data.summary?.requests_total || 0}
+ Active keys {data.summary?.active_keys || 0}
+
+
+
+
+
+
+ Email Plan Today Total Status
+
+ {(data.users || []).map((user) => (
+
+ {user.email} {user.plan} {user.requests_today} {user.requests_total} {user.banned ? 'Banned' : 'Active'}
+
+ ))}
+
+
+
+
+
+
+ Traffic
Top endpoints today
+
+ {(data.endpoints || []).map((item) =>
{item.endpoint}{item.requests}
)}
+
+
+
)
}
function App() {
+ const route = useHashRoute()
+ const [session, setSession] = useState(null)
+ const [authReady, setAuthReady] = useState(false)
+ const [authOpen, setAuthOpen] = useState(false)
+
useEffect(() => {
- const preventContextMenu = (event) => event.preventDefault()
- const preventCopyShortcuts = (event) => {
- if (event.ctrlKey && ['c', 'v', 'u'].includes(event.key.toLowerCase())) {
- event.preventDefault()
- }
- }
+ supabase.auth.getSession().then(({ data }) => {
+ setSession(data.session || null)
+ setAuthReady(true)
+ })
+ const { data: listener } = supabase.auth.onAuthStateChange((_event, nextSession) => {
+ setSession(nextSession)
+ setAuthReady(true)
+ })
+ return () => listener.subscription.unsubscribe()
+ }, [])
+ useEffect(() => {
+ const preventContextMenu = (event) => event.preventDefault()
document.addEventListener('contextmenu', preventContextMenu)
- document.addEventListener('keydown', preventCopyShortcuts)
-
- return () => {
- document.removeEventListener('contextmenu', preventContextMenu)
- document.removeEventListener('keydown', preventCopyShortcuts)
- }
+ return () => document.removeEventListener('contextmenu', preventContextMenu)
}, [])
+ const isAdmin = session?.user?.email?.toLowerCase() === ADMIN_EMAIL
+
+ async function signOut() {
+ await supabase.auth.signOut()
+ window.location.hash = 'home'
+ }
+
return (
- Code Uchiha
+
-
-
-
-
-
-
-
+ {route === 'dashboard' ? setAuthOpen(true)} />
+ : route === 'admin' ?
+ : setAuthOpen(true)} />}
+
+ setAuthOpen(false)} />
)
}
From cbe78650c7736d1248b8b3446dc713923e50c8be Mon Sep 17 00:00:00 2001
From: Takudzwa-Mlambo <157640942+Cod3Uchiha@users.noreply.github.com>
Date: Sun, 9 Aug 2026 07:55:59 +0200
Subject: [PATCH 4/4] Style auth and dashboard views
---
src/styles.css | 199 +++++++++++++++++++++++++++++++++++--------------
1 file changed, 141 insertions(+), 58 deletions(-)
diff --git a/src/styles.css b/src/styles.css
index dd29b8f..6b90bdb 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1,77 +1,160 @@
-* {
- box-sizing: border-box;
-}
-
-html,
-body,
-#root {
- min-height: 100%;
-}
+* { box-sizing: border-box; }
-body {
- margin: 0;
- background-color: #000;
- color: #ff0000;
- text-align: center;
+:root {
font-family: "Courier New", monospace;
+ color: #f5f5f5;
+ background: #050505;
+ font-synthesis: none;
}
-.site-shell {
- padding: 8px;
-}
+html, body, #root { min-height: 100%; }
+body { margin: 0; background: #050505; color: #f5f5f5; }
+a { color: inherit; }
+button, input { font: inherit; }
+button { cursor: pointer; }
-h1 {
- color: #f00;
- font-size: 36px;
-}
+.site-shell { min-height: 100vh; }
+.page { width: min(1120px, calc(100% - 32px)); margin: 0 auto; }
-.image-container {
- margin-top: 20px;
+.topbar {
+ position: sticky;
+ top: 0;
+ z-index: 20;
+ min-height: 68px;
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ align-items: center;
+ gap: 20px;
+ padding: 0 28px;
+ border-bottom: 1px solid #252525;
+ background: rgba(5, 5, 5, 0.94);
+ backdrop-filter: blur(18px);
}
-.image-container img {
- width: min(300px, 80vw);
- border-radius: 50%;
-}
+.brand { color: #f00; text-decoration: none; font-weight: 800; letter-spacing: .12em; justify-self: start; }
+.topbar nav { display: flex; align-items: center; gap: 22px; }
+.topbar nav a { text-decoration: none; color: #c8c8c8; font-size: 14px; }
+.topbar nav a:hover { color: #fff; }
+.auth-actions { justify-self: end; }
-.story {
- margin: 20px;
- padding: 20px;
- background-color: rgba(0, 0, 0, 0.8);
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+.primary-button, .secondary-button, .danger-button, .hacker-button {
+ border: 1px solid #f00;
+ border-radius: 8px;
+ padding: 10px 16px;
+ transition: .15s ease;
}
+.primary-button { background: #f00; color: #fff; }
+.primary-button:hover { background: #d80000; }
+.secondary-button, .hacker-button { background: transparent; color: #fff; }
+.secondary-button:hover, .hacker-button:hover { background: #171717; }
+.danger-button { background: transparent; color: #ff7a7a; border-color: #742929; }
+.danger-button:hover { background: #291111; }
+.compact { padding: 8px 12px; font-size: 13px; }
+.button-link { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; }
+.text-button, .icon-button { border: 0; background: transparent; color: #ff6b6b; }
+.icon-button { font-size: 28px; line-height: 1; }
-.audio-controls {
- margin-top: 20px;
+.hero {
+ min-height: 620px;
+ display: grid;
+ grid-template-columns: 1.25fr .75fr;
+ align-items: center;
+ gap: 70px;
+ text-align: left;
+ padding: 78px 0 52px;
}
+.hero h1, .page-heading h1, .empty-state h1 { margin: 8px 0 14px; color: #f00; font-size: clamp(40px, 7vw, 78px); line-height: .98; }
+.hero-copy { max-width: 720px; color: #c6c6c6; font-size: clamp(17px, 2vw, 21px); line-height: 1.65; }
+.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; }
+.public-note { margin-top: 18px; color: #777; font-size: 13px; line-height: 1.55; }
+.eyebrow { color: #ff5c5c; text-transform: uppercase; letter-spacing: .15em; font-size: 12px; }
+.image-container { text-align: center; }
+.image-container img { width: min(320px, 76vw); border-radius: 50%; border: 1px solid #3a1111; box-shadow: 0 0 80px rgba(255, 0, 0, .12); }
-.hacker-button {
- background-color: #000;
- color: #f00;
- border: 2px solid #f00;
- padding: 10px 20px;
- font: inherit;
- font-size: 16px;
- cursor: pointer;
- margin: 0 5px;
-}
+.quota-strip { display: grid; grid-template-columns: repeat(3, 1fr); border: 1px solid #232323; border-radius: 14px; overflow: hidden; margin-bottom: 42px; }
+.quota-strip > div { padding: 24px; display: flex; flex-direction: column; gap: 6px; border-right: 1px solid #232323; }
+.quota-strip > div:last-child { border-right: 0; }
+.quota-strip strong { color: #f00; font-size: 28px; }
+.quota-strip span { color: #969696; font-size: 13px; }
-.hacker-button:hover,
-.hacker-button:focus-visible {
- background-color: #f00;
- color: #fff;
-}
+.story, .dashboard-card { margin: 24px 0; padding: 28px; background: #0b0b0b; border: 1px solid #222; border-radius: 14px; }
+.story { text-align: left; line-height: 1.75; color: #bbb; }
+.story h2 { color: #fff; }
+.audio-controls { text-align: center; margin: 28px 0; }
+.contact-details { text-align: center; color: #bbb; padding: 18px 0; }
+.contact-details a { color: #ff4d4d; }
+.copyright { text-align: center; color: #666; padding: 34px 18px 46px; font-size: 12px; }
-.contact-details {
- margin-top: 20px;
- color: #fff;
-}
+.dashboard-page { padding-top: 62px; padding-bottom: 24px; }
+.page-heading { display: flex; align-items: end; justify-content: space-between; gap: 20px; margin-bottom: 28px; text-align: left; }
+.page-heading h1 { font-size: clamp(34px, 5vw, 58px); }
+.page-heading p { margin: 0; color: #888; }
+
+.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
+.stat-card { min-height: 132px; display: flex; flex-direction: column; justify-content: center; padding: 22px; border: 1px solid #242424; background: #0b0b0b; border-radius: 12px; text-align: left; }
+.stat-card span, .stat-card small { color: #838383; }
+.stat-card strong { margin: 8px 0; color: #fff; font-size: 27px; }
+
+.card-heading { display: flex; align-items: end; justify-content: space-between; gap: 18px; margin-bottom: 22px; text-align: left; }
+.card-heading h2 { margin: 5px 0 0; color: #fff; }
+.key-create-row { display: flex; gap: 8px; }
+.key-create-row input, .auth-form input { min-height: 42px; padding: 0 12px; border: 1px solid #303030; border-radius: 8px; background: #050505; color: #fff; outline: none; }
+.key-create-row input:focus, .auth-form input:focus { border-color: #f00; }
+
+.secret-box { display: grid; gap: 10px; padding: 16px; margin-bottom: 18px; border: 1px solid #5a2727; border-radius: 10px; background: #160808; text-align: left; }
+.secret-box code { overflow-wrap: anywhere; color: #ff8a8a; }
+.key-list, .usage-list { display: grid; gap: 8px; }
+.key-row, .usage-row { display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 14px; min-height: 58px; padding: 11px 14px; border: 1px solid #1f1f1f; border-radius: 9px; text-align: left; }
+.key-row > div { display: grid; gap: 5px; }
+.key-row code, .usage-row code { color: #ff6c6c; overflow-wrap: anywhere; }
+.key-row span { color: #888; font-size: 12px; }
+.usage-row { grid-template-columns: 1fr auto; }
+.usage-row strong { color: #fff; }
+.muted { color: #777; }
+
+.table-scroll { overflow-x: auto; }
+table { width: 100%; border-collapse: collapse; text-align: left; }
+th, td { padding: 13px 12px; border-bottom: 1px solid #222; white-space: nowrap; }
+th { color: #777; font-size: 12px; text-transform: uppercase; letter-spacing: .08em; }
+td { color: #c5c5c5; }
+
+.empty-state { width: min(680px, calc(100% - 32px)); margin: 0 auto; padding: 110px 0; text-align: center; }
+.empty-state h1 { font-size: 52px; }
+.empty-state p { color: #999; line-height: 1.6; }
+
+.modal-shell { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 18px; }
+.modal-backdrop { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: rgba(0, 0, 0, .78); backdrop-filter: blur(7px); }
+.auth-card { position: relative; z-index: 1; width: min(440px, 100%); padding: 26px; border: 1px solid #2d2d2d; border-radius: 14px; background: #0b0b0b; box-shadow: 0 30px 100px rgba(0,0,0,.6); text-align: left; }
+.auth-card-head { display: flex; justify-content: space-between; gap: 20px; }
+.auth-card h2 { color: #fff; margin: 5px 0 18px; font-size: 30px; }
+.auth-form { display: grid; gap: 14px; }
+.auth-form label { display: grid; gap: 7px; color: #aaa; font-size: 13px; }
+.form-error, .form-success, .panel-message { padding: 11px 13px; border-radius: 8px; font-size: 13px; }
+.form-error { color: #ffb0b0; background: #260b0b; border: 1px solid #5d2020; }
+.form-success { color: #b7ffbf; background: #0b2110; border: 1px solid #224a29; }
+.panel-message { margin-bottom: 16px; }
+.auth-card .text-button { display: block; margin: 18px auto 0; }
-.contact-details a {
- color: #f00;
+@media (max-width: 860px) {
+ .topbar { grid-template-columns: 1fr auto; padding: 0 16px; }
+ .topbar nav { order: 3; grid-column: 1 / -1; justify-content: center; padding-bottom: 12px; gap: 14px; flex-wrap: wrap; }
+ .hero { grid-template-columns: 1fr; gap: 30px; padding-top: 54px; text-align: center; }
+ .hero-copy { margin-left: auto; margin-right: auto; }
+ .hero-actions { justify-content: center; }
+ .quota-strip, .stat-grid { grid-template-columns: 1fr 1fr; }
+ .quota-strip > div { border-bottom: 1px solid #232323; }
+ .page-heading, .card-heading { align-items: stretch; flex-direction: column; }
+ .key-create-row { width: 100%; }
+ .key-create-row input { flex: 1; min-width: 0; }
}
-.copyright {
- margin-top: 20px;
- color: #fff;
+@media (max-width: 560px) {
+ .page { width: min(100% - 20px, 1120px); }
+ .quota-strip, .stat-grid { grid-template-columns: 1fr; }
+ .quota-strip > div { border-right: 0; }
+ .key-row { grid-template-columns: 1fr auto; }
+ .key-row > span { display: none; }
+ .key-create-row { flex-direction: column; }
+ .story, .dashboard-card { padding: 20px; }
+ .hero h1 { font-size: 48px; }
}