Changes include:

- Roboto Flex loaded from Google Fonts for compact UI text
- Reduced decorative borders while retaining useful input and divider borders
- Fixed blank journal detail pages
- Password-protected journal publisher with four-hour signed sessions
- New articles saved directly to posts.json
- Prominent Spotify, Instagram, and Steam buttons on Contact
- Desktop and mobile layouts visually verified
- Production build and authorization tests passed
This commit is contained in:
StormRunner06106
2026-09-11 11:35:01 -07:00
parent f80bceaa3c
commit 14d1dbbb07
15 changed files with 1507 additions and 32 deletions
+2 -1
View File
@@ -7,6 +7,7 @@ import BlogPage from "./pages/BlogPage";
import BlogPostPage from "./pages/BlogPostPage";
import ContactPage from "./pages/ContactPage";
import ExperiencePage from "./pages/ExperiencePage";
import JournalAdminPage from "./pages/JournalAdminPage";
import NotFoundPage from "./pages/NotFoundPage";
import SkillsPage from "./pages/SkillsPage";
@@ -45,6 +46,7 @@ export default function App() {
<Route path="/experience" element={<ExperiencePage />} />
<Route path="/skills" element={<SkillsPage />} />
<Route path="/blog" element={<BlogPage />} />
<Route path="/blog/manage" element={<JournalAdminPage />} />
<Route path="/blog/:slug" element={<BlogPostPage />} />
<Route
path="/contact"
@@ -55,4 +57,3 @@ export default function App() {
</Layout>
);
}
+19 -2
View File
@@ -17,7 +17,9 @@ async function request(path, options = {}) {
} catch {
// Keep the friendly fallback when a proxy or server returns non-JSON.
}
throw new Error(detail);
const error = new Error(detail);
error.status = response.status;
throw error;
}
return response.json();
@@ -28,9 +30,24 @@ export const getExperience = (signal) => request("/api/experience", { signal });
export const getSkills = (signal) => request("/api/skills", { signal });
export const getPosts = (signal) => request("/api/posts", { signal });
export const getPost = (slug, signal) => request(`/api/posts/${slug}`, { signal });
export const loginAdmin = (password) =>
request("/api/auth/login", {
method: "POST",
body: JSON.stringify({ password }),
});
export const getAdminSession = (token, signal) =>
request("/api/auth/session", {
signal,
headers: { Authorization: `Bearer ${token}` },
});
export const createPost = (payload, token) =>
request("/api/posts", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify(payload),
});
export const sendContactMessage = (payload) =>
request("/api/contact", {
method: "POST",
body: JSON.stringify(payload),
});
+28 -1
View File
@@ -27,10 +27,38 @@ const paths = {
<rect x="3" y="3" width="18" height="18" rx="4" />
</>
),
instagram: (
<>
<rect x="3" y="3" width="18" height="18" rx="5" />
<circle cx="12" cy="12" r="4" />
<path d="M17.5 6.5h.01" />
</>
),
lock: (
<>
<rect x="4" y="10" width="16" height="11" rx="3" />
<path d="M8 10V7a4 4 0 0 1 8 0v3M12 14v3" />
</>
),
logout: <path d="M10 5H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h5m4-4 3-3-3-3m3 3H8" />,
menu: <path d="M4 7h16M4 12h16M4 17h16" />,
plus: <path d="M12 5v14M5 12h14" />,
search: <path d="m20 20-4.35-4.35m2.35-5.15a7.5 7.5 0 1 1-15 0 7.5 7.5 0 0 1 15 0Z" />,
send: <path d="m21 3-7.5 18-3.8-7.7L2 9.5 21 3Zm-11.3 10.3L14 9" />,
spark: <path d="M12 2c.6 5.4 3.6 8.4 9 9-5.4.6-8.4 3.6-9 9-.6-5.4-3.6-8.4-9-9 5.4-.6 8.4-3.6 9-9Z" />,
spotify: (
<>
<circle cx="12" cy="12" r="9" />
<path d="M7.5 9.5c3.5-1 7.3-.6 10 1M8.3 12.5c2.8-.7 6-.4 8.4.8M9 15.3c2.2-.5 4.6-.2 6.6.8" />
</>
),
steam: (
<>
<circle cx="15.5" cy="8.5" r="3.5" />
<circle cx="7" cy="16.5" r="2.5" />
<path d="m9 15 3.7-2.4 2.8-.6M4.8 15.3 2.5 14" />
</>
),
};
export default function Icon({ name, size = 20, className = "" }) {
@@ -51,4 +79,3 @@ export default function Icon({ name, size = 20, className = "" }) {
</svg>
);
}
+12 -4
View File
@@ -51,11 +51,15 @@ export default function BlogPage() {
useEffect(() => {
const controller = new AbortController();
getPosts(controller.signal)
.then(setPosts)
.then((result) => {
if (!controller.signal.aborted) setPosts(result);
})
.catch((requestError) => {
if (requestError.name !== "AbortError") setError(requestError.message);
})
.finally(() => setLoading(false));
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, []);
@@ -80,7 +84,12 @@ export default function BlogPage() {
eyebrow="Journal"
title="Notes from the build."
description="Practical observations on applied AI, resilient products, and the technology choices behind them."
aside={<p className="issue-count">{posts.length || 10}<span>field notes</span></p>}
aside={(
<div className="journal-heading-aside">
<p className="issue-count">{posts.length || 10}<span>field notes</span></p>
<Link className="write-link" to="/blog/manage"><Icon name="plus" size={15} /> Write</Link>
</div>
)}
/>
<div className="journal-tools reveal">
@@ -129,4 +138,3 @@ export default function BlogPage() {
</section>
);
}
+11 -3
View File
@@ -21,12 +21,17 @@ export default function BlogPostPage() {
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError("");
getPost(slug, controller.signal)
.then(setPost)
.then((result) => {
if (!controller.signal.aborted) setPost(result);
})
.catch((requestError) => {
if (requestError.name !== "AbortError") setError(requestError.message);
})
.finally(() => setLoading(false));
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [slug]);
@@ -38,6 +43,10 @@ export default function BlogPostPage() {
return <div className="container content-page"><ErrorState message={error} /></div>;
}
if (!post) {
return <div className="container content-page"><ErrorState message="This journal note could not be opened." /></div>;
}
return (
<article className="article-page">
<header className={`article-hero article-hero--${post.accent}`}>
@@ -72,4 +81,3 @@ export default function BlogPostPage() {
</article>
);
}
+22 -1
View File
@@ -32,6 +32,7 @@ export default function ContactPage({ profile, error }) {
}
const socials = profile?.socials ?? [];
const interests = profile?.interests ?? [];
return (
<section className="content-page container contact-page">
@@ -120,6 +121,27 @@ export default function ContactPage({ profile, error }) {
<p>Im especially interested in full-stack product work, applied AI, and systems that make demanding workflows feel calmer.</p>
</div>
<div className="culture-card">
<p className="eyebrow">Beyond the build</p>
<h2><strong>I love music</strong> and nearly always have something playing on Spotify.</h2>
<p>Off the clock, I share a little life on Instagram and occasionally disappear into a game on Steam.</p>
<div className="culture-links">
{interests.map((interest) => (
<a
className={`culture-link culture-link--${interest.kind}`}
href={interest.href}
key={interest.kind}
rel="noreferrer"
target="_blank"
>
<span className="culture-link__icon"><Icon name={interest.kind} /></span>
<span><small>{interest.note}</small><strong>{interest.label}</strong></span>
<Icon name="arrowUpRight" size={18} />
</a>
))}
</div>
</div>
<div className="social-list">
<p className="eyebrow">Find me here</p>
{socials.map((social) => (
@@ -139,4 +161,3 @@ export default function ContactPage({ profile, error }) {
</section>
);
}
+210
View File
@@ -0,0 +1,210 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { createPost, getAdminSession, loginAdmin } from "../api";
import Icon from "../components/Icon";
import PageHeader from "../components/PageHeader";
import { LoadingState } from "../components/Status";
const TOKEN_KEY = "alex-journal-admin";
const today = new Date().toISOString().slice(0, 10);
const initialPost = {
title: "",
excerpt: "",
published_at: today,
read_time: 5,
tags: "AI, Engineering",
accent: "mint",
section_heading: "The idea",
body: "",
};
export default function JournalAdminPage() {
const navigate = useNavigate();
const [token, setToken] = useState(() => sessionStorage.getItem(TOKEN_KEY) ?? "");
const [checking, setChecking] = useState(Boolean(token));
const [password, setPassword] = useState("");
const [post, setPost] = useState(initialPost);
const [status, setStatus] = useState({ type: "idle", message: "" });
useEffect(() => {
if (!token) {
setChecking(false);
return undefined;
}
const controller = new AbortController();
getAdminSession(token, controller.signal)
.catch((error) => {
if (error.name !== "AbortError") {
sessionStorage.removeItem(TOKEN_KEY);
setToken("");
setStatus({ type: "error", message: "Your session ended. Sign in again." });
}
})
.finally(() => {
if (!controller.signal.aborted) setChecking(false);
});
return () => controller.abort();
}, [token]);
async function signIn(event) {
event.preventDefault();
setStatus({ type: "sending", message: "Signing in…" });
try {
const result = await loginAdmin(password);
sessionStorage.setItem(TOKEN_KEY, result.access_token);
setToken(result.access_token);
setPassword("");
setStatus({ type: "success", message: "Signed in. Your session lasts four hours." });
} catch (error) {
setStatus({ type: "error", message: error.message });
}
}
function signOut() {
sessionStorage.removeItem(TOKEN_KEY);
setToken("");
setStatus({ type: "idle", message: "" });
}
function updatePost(event) {
setPost((current) => ({ ...current, [event.target.name]: event.target.value }));
}
async function publish(event) {
event.preventDefault();
const paragraphs = post.body
.split(/\n\s*\n/)
.map((paragraph) => paragraph.trim())
.filter(Boolean);
const payload = {
title: post.title,
excerpt: post.excerpt,
published_at: post.published_at,
read_time: Number(post.read_time),
tags: post.tags.split(",").map((tag) => tag.trim()).filter(Boolean),
accent: post.accent,
content: [{ heading: post.section_heading, paragraphs }],
};
setStatus({ type: "sending", message: "Publishing…" });
try {
const created = await createPost(payload, token);
setStatus({ type: "success", message: "Published. Opening the article…" });
navigate(`/blog/${created.slug}`);
} catch (error) {
if (error.status === 401) {
sessionStorage.removeItem(TOKEN_KEY);
setToken("");
}
setStatus({ type: "error", message: error.message });
}
}
return (
<section className="content-page container publisher-page">
<PageHeader
eyebrow="Journal studio"
title="Publish a field note."
description="A small private writing room for adding new articles to the JSON journal."
aside={token ? <span className="publisher-badge"><span /> Authenticated</span> : null}
/>
{checking && <LoadingState label="Checking your session" />}
{!checking && !token && (
<form className="publisher-login reveal" onSubmit={signIn}>
<span className="publisher-login__icon"><Icon name="lock" size={24} /></span>
<div>
<p className="eyebrow">Private access</p>
<h2>Sign in to write</h2>
<p>The publisher uses one server-side password. It is never stored in the browser.</p>
</div>
<label>
<span>Admin password</span>
<input
autoComplete="current-password"
autoFocus
onChange={(event) => setPassword(event.target.value)}
placeholder="Enter your journal password"
required
type="password"
value={password}
/>
</label>
<button className="button button--primary" disabled={status.type === "sending"} type="submit">
Unlock publisher <Icon name="arrow" size={18} />
</button>
{status.message && <p className={`form-status form-status--${status.type}`} role="status">{status.message}</p>}
</form>
)}
{!checking && token && (
<form className="publisher-form reveal" onSubmit={publish}>
<div className="publisher-toolbar">
<div>
<p className="eyebrow">New article</p>
<p>Paragraphs are separated by a blank line.</p>
</div>
<button className="text-button" onClick={signOut} type="button"><Icon name="logout" size={16} /> Sign out</button>
</div>
<label className="publisher-field publisher-field--wide">
<span>Title</span>
<input minLength="5" name="title" onChange={updatePost} placeholder="A clear, useful title" required value={post.title} />
</label>
<label className="publisher-field publisher-field--wide">
<span>Excerpt</span>
<textarea maxLength="360" minLength="20" name="excerpt" onChange={updatePost} placeholder="A concise introduction for the journal index." required rows="3" value={post.excerpt} />
</label>
<div className="publisher-fields-row">
<label className="publisher-field">
<span>Publish date</span>
<input name="published_at" onChange={updatePost} required type="date" value={post.published_at} />
</label>
<label className="publisher-field">
<span>Reading time</span>
<input max="60" min="1" name="read_time" onChange={updatePost} required type="number" value={post.read_time} />
</label>
<label className="publisher-field">
<span>Color</span>
<select name="accent" onChange={updatePost} value={post.accent}>
<option value="mint">Mint</option>
<option value="blue">Blue</option>
<option value="lavender">Lavender</option>
<option value="peach">Peach</option>
<option value="yellow">Yellow</option>
</select>
</label>
</div>
<label className="publisher-field publisher-field--wide">
<span>Topics</span>
<input name="tags" onChange={updatePost} placeholder="AI, React, Systems" required value={post.tags} />
<small>Separate up to six topics with commas.</small>
</label>
<label className="publisher-field publisher-field--wide">
<span>Section heading</span>
<input minLength="3" name="section_heading" onChange={updatePost} required value={post.section_heading} />
</label>
<label className="publisher-field publisher-field--wide">
<span>Article</span>
<textarea minLength="40" name="body" onChange={updatePost} placeholder="Write the article here…" required rows="15" value={post.body} />
</label>
<div className="publisher-submit">
<button className="button button--primary" disabled={status.type === "sending"} type="submit">
<Icon name="plus" size={18} /> {status.type === "sending" ? "Publishing…" : "Publish article"}
</button>
{status.message && <p className={`form-status form-status--${status.type}`} role="status">{status.message}</p>}
</div>
</form>
)}
</section>
);
}
+323 -14
View File
@@ -25,7 +25,7 @@
--container: 1160px;
--gutter: clamp(24px, 8vw, 150px);
color: var(--ink);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-family: "Roboto Flex", Roboto, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
@@ -171,7 +171,7 @@ button {
align-items: center;
gap: 3px;
padding: 5px;
border: 1px solid rgba(54, 78, 70, 0.08);
border: 0;
border-radius: 18px;
background: rgba(255, 255, 255, 0.67);
box-shadow: 0 8px 28px rgba(46, 65, 58, 0.05);
@@ -650,7 +650,7 @@ button {
.timeline-card {
overflow: hidden;
border: 1px solid var(--line);
border: 0;
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.72);
box-shadow: 0 10px 38px rgba(46, 65, 58, 0.04);
@@ -728,7 +728,7 @@ button {
height: 38px;
flex: 0 0 auto;
place-items: center;
border: 1px solid var(--line);
border: 0;
border-radius: 13px;
background: rgba(247, 248, 244, 0.78);
transition: transform 200ms ease, background 200ms ease;
@@ -809,7 +809,7 @@ button {
justify-content: space-between;
gap: 10px;
padding: 12px 13px;
border: 1px solid var(--line);
border: 0;
border-radius: 14px;
background: #fafbf8;
transition: background 160ms ease, transform 160ms ease;
@@ -874,7 +874,7 @@ button {
gap: 30px;
margin-bottom: 22px;
padding: 25px 28px;
border: 1px solid var(--line);
border: 0;
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.58);
}
@@ -919,7 +919,7 @@ button {
grid-column: span 2;
min-height: 285px;
padding: 24px;
border: 1px solid rgba(54, 78, 70, 0.07);
border: 0;
border-radius: var(--radius-lg);
animation-delay: var(--delay);
transition: transform 200ms ease, box-shadow 200ms ease;
@@ -999,7 +999,7 @@ button {
.issue-count span {
color: var(--ink-faint);
font-family: Inter, ui-sans-serif, sans-serif;
font-family: "Roboto Flex", Roboto, ui-sans-serif, sans-serif;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
@@ -1073,7 +1073,7 @@ button {
.filter-row button:hover,
.filter-row button.is-active {
border-color: var(--line);
border-color: transparent;
color: var(--ink);
background: rgba(255, 255, 255, 0.72);
}
@@ -1086,7 +1086,7 @@ button {
.post-card {
overflow: hidden;
border: 1px solid rgba(54, 78, 70, 0.08);
border: 0;
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.74);
box-shadow: 0 8px 28px rgba(46, 65, 58, 0.04);
@@ -1231,8 +1231,9 @@ button {
height: 34px;
flex: 0 0 auto;
place-items: center;
border: 1px solid var(--line);
border: 0;
border-radius: 12px;
background: rgba(225, 238, 233, 0.65);
transition: color 160ms ease, background 160ms ease;
}
@@ -1407,7 +1408,7 @@ button {
justify-items: center;
margin-top: 70px;
padding: 36px;
border: 1px solid var(--line);
border: 0;
border-radius: var(--radius-lg);
background: var(--surface);
text-align: center;
@@ -1458,7 +1459,7 @@ button {
display: grid;
gap: 19px;
padding: clamp(25px, 4vw, 40px);
border: 1px solid var(--line);
border: 0;
border-radius: var(--radius-xl);
background: rgba(255, 255, 255, 0.78);
box-shadow: var(--shadow-soft);
@@ -1551,7 +1552,7 @@ button {
.contact-note,
.social-list {
padding: 25px;
border: 1px solid var(--line);
border: 0;
border-radius: var(--radius-lg);
}
@@ -1692,6 +1693,283 @@ button {
to { transform: rotate(360deg); }
}
.journal-heading-aside {
display: flex;
align-items: center;
gap: 18px;
}
.write-link,
.text-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
border: 0;
color: var(--sage-deep);
background: var(--mint);
font-size: 11px;
font-weight: 780;
cursor: pointer;
}
.write-link {
min-height: 38px;
padding-inline: 14px;
border-radius: 13px;
box-shadow: 0 8px 22px rgba(47, 81, 71, 0.08);
}
.culture-card {
padding: 25px;
border-radius: var(--radius-lg);
background: linear-gradient(145deg, var(--lavender), #f4f0f5);
box-shadow: 0 12px 34px rgba(75, 67, 95, 0.06);
}
.culture-card .eyebrow {
margin-bottom: 13px;
color: var(--lavender-deep);
}
.culture-card h2 {
margin: 0;
color: var(--ink);
font-family: "Roboto Flex", Roboto, ui-sans-serif, sans-serif;
font-size: 18px;
font-weight: 520;
letter-spacing: -0.025em;
line-height: 1.3;
}
.culture-card h2 strong {
font-weight: 850;
}
.culture-card > p:not(.eyebrow) {
margin: 11px 0 20px;
color: var(--ink-soft);
font-size: 12px;
line-height: 1.65;
}
.culture-links {
display: grid;
gap: 9px;
}
.culture-link {
display: grid;
min-height: 58px;
grid-template-columns: 38px minmax(0, 1fr) auto;
align-items: center;
gap: 11px;
padding: 10px 12px;
border-radius: 16px;
color: var(--ink);
box-shadow: 0 9px 24px rgba(46, 50, 58, 0.07);
transition: transform 180ms ease, box-shadow 180ms ease;
}
.culture-link:hover {
box-shadow: 0 13px 29px rgba(46, 50, 58, 0.11);
transform: translateY(-2px);
}
.culture-link--spotify { background: #dff3e5; }
.culture-link--instagram { background: #f6e3ea; }
.culture-link--steam { background: #e0ebf3; }
.culture-link__icon {
display: grid;
width: 38px;
height: 38px;
place-items: center;
border-radius: 13px;
background: rgba(255, 255, 255, 0.7);
}
.culture-link small,
.culture-link strong {
display: block;
}
.culture-link small {
margin-bottom: 2px;
color: var(--ink-faint);
font-size: 8px;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
}
.culture-link strong {
font-size: 12px;
font-style: italic;
font-weight: 850;
}
.publisher-badge {
display: inline-flex;
align-items: center;
gap: 9px;
padding: 10px 13px;
border-radius: 999px;
color: #3b725e;
background: var(--mint);
font-size: 10px;
font-weight: 750;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.publisher-badge span {
width: 7px;
height: 7px;
border-radius: 50%;
background: #5c947c;
}
.publisher-login {
display: grid;
max-width: 620px;
gap: 22px;
margin: -22px auto 30px;
padding: clamp(28px, 5vw, 48px);
border-radius: var(--radius-xl);
background: rgba(255, 255, 255, 0.8);
box-shadow: var(--shadow);
}
.publisher-login__icon {
display: grid;
width: 52px;
height: 52px;
place-items: center;
border-radius: 17px;
color: var(--sage-deep);
background: var(--mint);
}
.publisher-login .eyebrow,
.publisher-toolbar .eyebrow {
margin-bottom: 9px;
}
.publisher-login h2 {
margin: 0;
font-family: Georgia, "Times New Roman", serif;
font-size: 34px;
font-weight: 400;
letter-spacing: -0.04em;
}
.publisher-login > div > p:last-child,
.publisher-toolbar > div > p:last-child {
margin: 8px 0 0;
color: var(--ink-soft);
font-size: 12px;
line-height: 1.6;
}
.publisher-login label,
.publisher-field {
display: grid;
gap: 8px;
}
.publisher-login label > span,
.publisher-field > span {
color: var(--ink-soft);
font-size: 10px;
font-weight: 760;
letter-spacing: 0.07em;
text-transform: uppercase;
}
.publisher-login input,
.publisher-field input,
.publisher-field textarea,
.publisher-field select {
width: 100%;
border: 1px solid rgba(54, 78, 70, 0.13);
border-radius: 15px;
outline: 0;
color: var(--ink);
background: #fbfcfa;
font-size: 13px;
transition: border 160ms ease, box-shadow 160ms ease, background 160ms ease;
}
.publisher-login input,
.publisher-field input,
.publisher-field select {
min-height: 50px;
padding-inline: 15px;
}
.publisher-field textarea {
padding: 15px;
line-height: 1.7;
resize: vertical;
}
.publisher-login input:focus,
.publisher-field input:focus,
.publisher-field textarea:focus,
.publisher-field select:focus {
border-color: rgba(47, 81, 71, 0.43);
background: white;
box-shadow: 0 0 0 4px rgba(47, 81, 71, 0.07);
}
.publisher-form {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 21px;
margin-top: -25px;
padding: clamp(28px, 5vw, 48px);
border-radius: var(--radius-xl);
background: rgba(255, 255, 255, 0.8);
box-shadow: var(--shadow-soft);
}
.publisher-toolbar,
.publisher-field--wide,
.publisher-submit {
grid-column: 1 / -1;
}
.publisher-toolbar,
.publisher-submit {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.text-button {
padding: 10px 13px;
border-radius: 12px;
}
.publisher-fields-row {
display: grid;
grid-column: 1 / -1;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.publisher-field small {
color: var(--ink-faint);
font-size: 10px;
}
.publisher-submit {
justify-content: flex-start;
padding-top: 4px;
}
@media (max-width: 1120px) {
:root {
--gutter: clamp(24px, 5vw, 70px);
@@ -1782,6 +2060,10 @@ button {
.contact-aside {
grid-template-columns: 1fr 1fr;
}
.contact-aside .social-list {
grid-column: 1 / -1;
}
}
@media (max-width: 720px) {
@@ -1940,6 +2222,14 @@ button {
grid-template-columns: 1fr;
}
.publisher-fields-row {
grid-template-columns: 1fr;
}
.publisher-toolbar {
align-items: flex-start;
}
.form-footer {
align-items: stretch;
flex-direction: column;
@@ -1957,6 +2247,25 @@ button {
font-size: 9px;
}
.journal-heading-aside {
flex-wrap: wrap;
}
.publisher-login,
.publisher-form {
margin-top: -10px;
padding: 24px;
}
.publisher-submit {
align-items: stretch;
flex-direction: column;
}
.publisher-submit .button {
width: 100%;
}
.about-page {
gap: 40px;
padding-block: 50px 70px;