srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/styles.css
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-02-25 09:23:00 +0200
committersrdusr <[email protected]>2026-02-25 09:23:00 +0200
commitd7c1c7b35e9d0995eed41d437404d5255529cf17 (patch)
treeda9fe82110da644d8150ae520ca70cfda23d1c6f /web/src/styles.css
parent4933f669f8338ca5e4c64cf0294818c5d15ed122 (diff)
downloadtyperpunk-d7c1c7b35e9d0995eed41d437404d5255529cf17.tar.gz
typerpunk-d7c1c7b35e9d0995eed41d437404d5255529cf17.zip
Give TyperPunk a mark, and fix the settings and custom text dialogs
Branding. The name was set in the body typeface and that was the whole of it. There is a mark now: a chevron, a baseline, and a block cursor resting on it, which reads as text being entered at a prompt and stays legible at 16 pixels. The chevron is drawn twice, the lower copy offset and in the secondary colour, which is the one stylistic note. The wordmark is split so the accent falls on the second half. Everything is drawn in the theme's own variables, so the mark follows the palette rather than carrying its own, and the favicon is the same drawing. The menu carried the name twice, as its own title and as the fixed corner mark. The corner mark exists for screens with no title of their own, so the menu keeps only the title, now the full lockup at size. The settings panel. Measured at 1280x900 it opened at y=506 with a height of 437, so it ran 140px past the bottom of a 900px window, and .app is overflow:hidden so there was nothing to scroll. It had no background either, so the fixed footer links rendered through the middle of it and, sitting higher in the stacking order, took the clicks: Playwright could not press the panel's own Close button because the footer intercepted every attempt. Both it and the custom text panel are dialogs now. Centred, opaque, above the fixed chrome, scrolling internally, with the page dimmed behind them. Their controls are buttons, but .quiet removes a button's border, so a panel of them read as a list of plain text labels with nothing to suggest any of it could be clicked. They keep an outline inside these panels. The checkbox was the browser's default control in a panel where nothing else was, and its border was too dark to see against the panel. Escape did not close either of them. The handler looked for .rail-settings-panel, which is a different panel on a different screen. A Dev section with a button that jumps straight to the results screen with invented figures was shipping to every visitor. It is limited to localhost. The top rail. The account control has moved into the icon row, right of Friends, and is the account's own picture once signed in: there is no avatar upload, so it is the initial over a colour hashed from the whole username, which gives every account a stable and distinct mark. Sign In and Sign Up sit on their own row beneath. The friends count is a badge under its icon rather than the words "2 online" beside it. That took the rail from 307px wide to 189px, which is what had been overlapping the wordmark by 75px at 390px. But it also made the rail taller, which put the close button back underneath it, so the rail now publishes its measured height and the content column starts below whatever that is. The rail is one row signed in and two signed out, so this is not a constant the stylesheet could hold. The wordmark was inline-flex, which blockified its two halves, so the name read as two lines to innerText: a screen reader, a copy-paste, or a test. The advertising slot has moved from inside the results screen to a banner across the top of the page, where a navigation bar usually goes. The header moves down to make room rather than being covered by it. It stays off the typing screen, the passive reader and the race lobby. Custom text: the panel had no title, and its hint was centred at 340px inside a 480px panel, which broke one sentence into three ragged lines. 3 browser tests pass. The store test now asserts the avatar replaces the sign-in links rather than expecting a button that has moved.
Diffstat (limited to 'web/src/styles.css')
-rw-r--r--web/src/styles.css317
1 files changed, 280 insertions, 37 deletions
diff --git a/web/src/styles.css b/web/src/styles.css
index b47f21a..f51bb65 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -15,7 +15,12 @@
so nothing below has to know whether a banner exists. */
--ad-banner-height: 6.5rem;
--ad-banner-h: 0px;
- --header-offset: calc(3.5rem + var(--ad-banner-h));
+ /* Set from the measured top rail by renderTopRail, because the rail is
+ one row signed in and two signed out. The fallback covers the moment
+ before the first measurement lands. */
+ --top-rail-h: 2.5rem;
+ /* Where content starts: below the banner, below the rail, plus a gap. */
+ --header-offset: calc(1rem + var(--top-rail-h) + 0.75rem + var(--ad-banner-h));
/* App padding variable so we can compensate where needed */
--app-padding: 1rem;
/* Room at the foot of every screen for the fixed footer and the corner
@@ -205,17 +210,87 @@ body {
overflow: hidden;
}
+/* The mark and wordmark together, fixed at the top-left of every screen. */
.logo {
position: fixed;
top: calc(1rem + var(--ad-banner-h));
left: 1rem;
- font-size: 1.2rem;
- color: var(--primary-color);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 1.15rem;
font-family: 'JetBrains Mono', monospace;
text-transform: uppercase;
letter-spacing: 2px;
cursor: pointer;
- transition: color 0.2s ease;
+ line-height: 1;
+}
+
+.logo-mark {
+ width: 1.5em;
+ height: 1.5em;
+ flex-shrink: 0;
+ overflow: visible;
+}
+
+.logo-mark-chevron,
+.logo-mark-baseline,
+.logo-mark-cursor {
+ stroke: var(--primary-color);
+ fill: var(--primary-color);
+}
+
+.logo-mark-chevron {
+ fill: none;
+}
+
+/* The offset copy underneath. Faint enough to read as a print misregistration
+ rather than a second chevron. */
+.logo-mark-ghost {
+ stroke: var(--secondary-color, var(--primary-color));
+ opacity: 0.55;
+}
+
+/* Plain inline, not a flex container. As inline-flex the two halves became
+ flex items, which blockifies them, and the name read as "TYPER" then
+ "PUNK" on separate lines to anything using innerText: a screen reader, a
+ copy-paste, or a test. */
+.logo-word {
+ display: inline;
+ white-space: nowrap;
+}
+
+.logo-word-a {
+ color: var(--text-color);
+}
+
+.logo-word-b {
+ color: var(--primary-color);
+}
+
+.logo:hover .logo-word-a {
+ color: var(--primary-color);
+}
+
+.logo:hover .logo-word-b {
+ color: var(--secondary-color, var(--primary-color));
+}
+
+/* The cursor blinks, at the same rate as the typing caret, because it is the
+ same idea. Anyone who has asked not to see motion gets it held still. */
+@keyframes logo-cursor-blink {
+ 0%, 45% { opacity: 1; }
+ 55%, 100% { opacity: 0.15; }
+}
+
+.logo-mark-cursor {
+ animation: logo-cursor-blink 1.6s steps(1, end) infinite;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .logo-mark-cursor {
+ animation: none;
+ }
}
.logo:hover {
@@ -232,8 +307,10 @@ body {
so on anything but a tall viewport the list ran past the bottom edge.
Fixed higher, there's always real room underneath it. */
justify-content: flex-start;
- padding-top: 6rem;
- height: 100vh;
+ /* Plus the banner, which is fixed over the top of the page. Without the
+ addition the title cleared it by 8px. */
+ padding-top: calc(6rem + var(--ad-banner-h));
+ min-height: 100vh;
width: 100%;
max-width: 700px;
margin: 0 auto;
@@ -244,12 +321,23 @@ body {
box-sizing: border-box;
}
-.main-menu h1 {
- font-size: 4rem;
- color: var(--primary-color);
+/* The menu's own title is the full lockup at size. It is not fixed to the
+ corner like the one every other screen carries: the menu had both, so the
+ name appeared twice on the same screen. */
+.logo-hero {
+ position: static;
+ font-size: 3.25rem;
+ gap: 0.75rem;
margin-bottom: 2rem;
- text-transform: uppercase;
- letter-spacing: 2px;
+ cursor: default;
+}
+
+.logo-hero:hover .logo-word-a {
+ color: var(--text-color);
+}
+
+.logo-hero:hover .logo-word-b {
+ color: var(--primary-color);
}
.settings-row-group {
@@ -721,11 +809,15 @@ body {
were the same control under two names. .quiet is the one with a stated
role, and is defined with the rest of the hierarchy below. */
+/* Left aligned, and as wide as the panel. Centred at 340px inside a 480px
+ panel it broke into three ragged lines that were harder to read than the
+ sentence deserved. */
.custom-hint {
font-size: 0.8rem;
- color: var(--neutral-color);
- text-align: center;
- max-width: 340px;
+ line-height: 1.5;
+ color: var(--sub-color);
+ text-align: left;
+ width: 100%;
}
.settings-panel {
@@ -734,8 +826,6 @@ body {
display: flex;
flex-direction: column;
align-items: center;
- /* Tightened alongside the two-column split: .app is overflow:hidden, so
- anything past the viewport bottom is clipped rather than scrollable. */
gap: 0.55rem;
border: var(--border-width) solid var(--neutral-color);
border-radius: var(--radius-panel);
@@ -744,6 +834,54 @@ body {
text-align: center;
}
+/* The settings controls are buttons, but .quiet removes their border, so a
+ panel of them read as a list of plain text labels with no sign that any of
+ it could be clicked. Inside this panel they keep an outline. */
+.settings-panel .menu-button.quiet,
+.custom-panel .menu-button.quiet {
+ border-color: var(--border-color);
+ color: var(--text-color);
+}
+
+.settings-panel .menu-button.quiet:hover,
+.custom-panel .menu-button.quiet:hover {
+ border-color: var(--primary-color);
+}
+
+/* The checkbox was the browser's default control in a panel where nothing
+ else was. */
+.settings-row {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.85rem;
+ color: var(--sub-color);
+ cursor: pointer;
+}
+
+.settings-row input[type="checkbox"] {
+ appearance: none;
+ width: 1rem;
+ height: 1rem;
+ margin: 0;
+ /* A dark border on a dark panel was invisible. This is a control, so it
+ has to look like one before it is checked, not only after. */
+ border: var(--border-width) solid var(--sub-color);
+ border-radius: 2px;
+ background: transparent;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.settings-row input[type="checkbox"]:checked {
+ background: var(--primary-color);
+ border-color: var(--primary-color);
+}
+
+.settings-row:hover input[type="checkbox"] {
+ border-color: var(--primary-color);
+}
+
.settings-row {
display: flex;
align-items: center;
@@ -802,6 +940,15 @@ body {
.theme-swatch-preview[data-swatch="gruvbox"] { background: linear-gradient(90deg, #282828 60%, #fabd2f 60%); }
.theme-swatch-preview[data-swatch="monokai"] { background: linear-gradient(90deg, #272822 60%, #a6e22e 60%); }
+/* A dialog says what it is. Both of these opened with no title at all. */
+.panel-title {
+ font-size: 1rem;
+ color: var(--text-color);
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ text-align: left;
+}
+
.custom-panel {
width: 100%;
max-width: 480px;
@@ -814,6 +961,52 @@ body {
margin: 1.5rem auto 0 auto;
}
+/* Both of these are dialogs, and neither was built as one.
+
+ They sat in the menu's normal flow, below the Start buttons, which put the
+ settings panel at y=506 with a height of 437 on a 900px viewport: it ran
+ 140px off the bottom, and .app is overflow:hidden so there was nothing to
+ scroll. Worse, neither had a background, so the fixed footer links rendered
+ through the middle of them and, being higher in the stacking order,
+ swallowed the clicks. Playwright could not press the settings panel's own
+ Close button: the footer intercepted every attempt.
+
+ Centred, opaque, above the fixed chrome, and scrolling internally when the
+ content is taller than the screen. */
+.settings-panel:not([hidden]),
+.custom-panel:not([hidden]) {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 120;
+ margin: 0;
+ max-height: min(82vh, 44rem);
+ overflow-y: auto;
+ background: var(--background-color);
+ border-color: var(--border-color);
+ box-sizing: border-box;
+ /* Dims the page behind without needing a separate backdrop element. */
+ box-shadow: 0 0 0 100vmax color-mix(in srgb, var(--background-color) 78%, transparent);
+}
+
+/* The settings controls are buttons, but .quiet removes their border, so a
+ panel of them read as a list of plain text labels with nothing to suggest
+ any of it could be clicked. Inside these panels they keep an outline. */
+.settings-panel .menu-button.quiet,
+.custom-panel .menu-button.quiet,
+.rail-settings-panel .menu-button.quiet {
+ border-color: var(--border-color);
+ color: var(--text-color);
+}
+
+.settings-panel .menu-button.quiet:hover,
+.custom-panel .menu-button.quiet:hover,
+.rail-settings-panel .menu-button.quiet:hover {
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
.custom-textarea {
width: 100%;
min-height: 120px;
@@ -1207,14 +1400,25 @@ body {
/* How many friends are online, beside the Friends control in the top-right
rail. Readable text rather than a corner badge, so it can say what it
counts. */
+/* How many friends are online, under the Friends icon. A bare number: the
+ icon above it already says what is being counted. */
.online-count {
- font-size: 0.75rem;
- letter-spacing: 1px;
- text-transform: uppercase;
- color: var(--primary-color);
+ min-width: 1.1rem;
+ padding: 0 0.25rem;
+ border-radius: var(--radius-pill, 999px);
+ background: var(--primary-color);
+ color: var(--background-color);
+ font-size: 0.65rem;
+ font-weight: 700;
+ line-height: 1.25;
+ text-align: center;
white-space: nowrap;
}
+.online-count[hidden] {
+ display: none;
+}
+
.corner-rail-right {
position: fixed;
bottom: 1rem;
@@ -1297,8 +1501,8 @@ body {
/* Mobile adjustments */
@media (max-width: 768px) {
- .main-menu h1 {
- font-size: 2.5rem;
+ .logo-hero {
+ font-size: 2.1rem;
margin-bottom: 1.5rem;
}
@@ -2260,9 +2464,19 @@ body::-webkit-scrollbar, .app::-webkit-scrollbar, #root::-webkit-scrollbar {
top: calc(1rem + var(--ad-banner-h));
right: 1rem;
z-index: 100;
+ /* Two rows: the icons, and the sign-in links under them. As one row the
+ two text links made the rail 307px wide, which is what collided with
+ the wordmark on a phone. */
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 0.25rem;
+}
+
+.top-rail-icons {
display: flex;
flex-direction: row;
- align-items: center;
+ align-items: flex-start;
gap: 0.6rem;
}
@@ -2339,7 +2553,43 @@ body::-webkit-scrollbar, .app::-webkit-scrollbar, #root::-webkit-scrollbar {
.auth-control {
display: flex;
align-items: center;
- gap: 0.4rem;
+ gap: 0.3rem;
+}
+
+.auth-control[hidden] {
+ display: none;
+}
+
+.top-rail-sep {
+ color: color-mix(in srgb, var(--sub-color) 55%, transparent);
+ font-size: 0.75rem;
+}
+
+/* Friends sits above its count, so the count does not widen the icon row. */
+.friends-control,
+.account-icon-control {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.15rem;
+}
+
+/* The account's own picture once signed in. There is no upload, so it is the
+ initial over a colour derived from the whole username. */
+.rail-avatar {
+ background: var(--avatar-colour, var(--primary-color));
+ border-color: transparent;
+ color: #fff;
+}
+
+.rail-avatar:hover {
+ border-color: var(--text-color);
+}
+
+.rail-avatar-initial {
+ font-size: 0.85rem;
+ font-weight: 700;
+ line-height: 1;
}
/* Text links, not bordered boxes - three outlined squares plus two outlined
@@ -2958,11 +3208,10 @@ body::-webkit-scrollbar, .app::-webkit-scrollbar, #root::-webkit-scrollbar {
wordmark and Escape already did, and the least reachable of the three. */
.screen-close {
position: absolute;
- /* The top rail is fixed to the viewport's top-right and is wide enough to
- reach into the content column on any viewport narrower than about
- 1350px, which is most of them. At top: 0 the close button sat directly
- underneath the rail's buttons. It starts below the rail instead. */
- top: 1.5rem;
+ /* The content column already starts below the rail, so a small offset
+ inside it is enough. The rail's own height feeds --header-offset, which
+ is what puts this clear of it whether the rail has one row or two. */
+ top: 0.25rem;
right: 0;
width: 2.25rem;
height: 2.25rem;
@@ -3340,7 +3589,7 @@ body::-webkit-scrollbar, .app::-webkit-scrollbar, #root::-webkit-scrollbar {
letter-spacing: 1px;
}
- .top-rail {
+ .top-rail-icons {
gap: 0.35rem;
}
@@ -3349,14 +3598,8 @@ body::-webkit-scrollbar, .app::-webkit-scrollbar, #root::-webkit-scrollbar {
padding: 0.5rem 0.2rem;
}
- /* Sign Up goes. Sign In reaches the same screen, which offers both, so
- the second link only costs width. */
- .top-rail-link.accent {
- display: none;
- }
-
.screen-close {
- top: 2.75rem;
+ top: 0.25rem;
}
}