--- version: alpha name: keyway-design description: "A warm paper canvas (#f4efe6) with near-black ink (#1c1a17), the first cream/brown trade-shop palette in the gallery distinct from the neon or pastel batches around it. Brass (#8a5f1f) is the working accent, rust red (#c0392b) is reserved for the one alarm state. Oswald carries every headline in uppercase, Karla carries prose and controls. The homepage is built around a pin-tumbler cylinder: five pins, cut to five real bitting depths, drop to a shared shear line, and the cylinder only turns open once every pin has landed." --- ## Brand Positioning Keyway is a locksmith, and it exists because most trade sites sell fear before they sell a service: a stock photo of a stranger's hand on a doorknob, a headline about how unsafe you are, and a phone number with no price attached to anything. Keyway's bet is the opposite: show the actual mechanism, a lock only opens when every pin clears the shear line, and let that same standard apply to the business side, a real price and a real time estimate for every job, before anyone calls. If a component performs "peace of mind" without naming a real service, a real price, or the pins themselves, it doesn't belong here. colors: accent: "#8a5f1f" accent-alt: "#c0392b" ink: "#1c1a17" ink-muted: "#6b5a3f" canvas: "#f4efe6" ## Logo / Wordmark System The mark is a keyway in cross-section: a portrait housing outline with three cut notches at different depths inside it, one dashed shear line crossing near the top. It is not a padlock, a house silhouette, or a shield. It is a miniature reading of the same cylinder the homepage is built around. ```svg ``` - **Lockup**: mark, then a `0.5rem` gap, then "Keyway" in Oswald 700, uppercase, `.03em` tracking. Mark always left of the wordmark, never stacked. - **Minimum size**: mark not smaller than `18px` tall, below that the three notch depths stop reading as different lengths. - **Clearspace**: empty margin equal to the mark's own width on all four sides. - **Don't**: don't recolor the housing outline, it is always `--ink`. Don't make all three notches the same depth, the whole point of the mark is that they aren't. Don't drop the dashed shear line, without it the mark is just three bars in a box. ## Color System ### Roles | Role | Token | Value | Used for | |---|---|---|---| | Canvas | `--bg` | `#f4efe6` | Page background, channel wells in the cylinder graphic | | Surface | `--panel` | `#ffffff` | Nav, service cards, table, form card | | Surface raised | `--raised` | `#f9f5ee` | Row hover, table header, notice fill on hover-adjacent states | | Border | `--border` | `#e2d8c4` | Card edges, table rules, dividers | | Border strong | `--border-strong` | `#c9b998` | Decorative-only edges: card top rule, unset pin fill, housing stroke | | Border UI | `--border-ui` | `#8a7452` | Every interactive boundary: secondary button, input, select | | Ink | `--ink` | `#1c1a17` | Headlines, body copy, table cells | | Ink muted | `--dim` | `#6b5a3f` | Labels, meta lines, helper text | | Brass | `--accent` | `#8a5f1f` | Primary button, mark fill, set-pin fill, "on call"/"same day" status, links | | Rust | `--accent-alt` | `#c0392b` | Shear line once unlocked, form errors, the "UNLOCKED" readout | The two accents split by job, not by decoration. Brass is the working color: it's what a pin turns once it's set, what the primary button is filled with, what an available job status reads in. Rust is reserved for one thing, attention at the moment of change, the shear line's color once the cylinder turns, and a form error. There is no green "all clear" color, because a job being "on call" is a fact stated in words, not a traffic light. ### Contrast (computed from the hex values above via WCAG relative luminance) | Pairing | Ratio | Passes | |---|---|---| | `--ink` on `--bg` (headlines, body copy) | 15.16:1 | AAA | | `--ink` on `--panel` (card copy, table cells) | 17.36:1 | AAA | | `--ink` on `--raised` (hovered row) | 15.98:1 | AAA | | `--dim` on `--bg` (labels, helper text) | 5.80:1 | AA | | `--dim` on `--panel` (meta lines, footer) | 6.65:1 | AA | | `--accent` on `--bg` (links, brass text) | 4.91:1 | AA | | `--accent` on `--panel` (links inside a card) | 5.62:1 | AA | | `--accent-alt` on `--bg` (error text, unlocked label) | 4.75:1 | AA | | `--accent-alt` on `--panel` | 5.44:1 | AA | | white label on `--accent` fill (primary button) | 5.62:1 | AA | | white label on `--accent-alt` fill (not currently used as a fill, kept for future error-state buttons) | 5.44:1 | AA | | `--border-ui` control edge on `--bg` (non-text, inputs) | 3.90:1 | AA (1.4.11) | | `--border-ui` control edge on `--panel` (non-text, secondary button) | 4.47:1 | AA (1.4.11) | | `--dim` label on `--accent` fill (never paired, checked for safety) | 3.09:1 | fails as body text, not used this way | `--border-strong` at 1.69:1 against canvas is deliberately kept for decorative edges only: the card's top rule, an unset pin's fill, and the cylinder housing's stroke in the hero graphic. None of those are things a person clicks, types in, or drags, so the 3:1 non-text minimum doesn't apply to them, and `--border-ui` exists precisely so an actual control never inherits that weaker value. The hero cylinder itself is marked `aria-hidden`, for the same reason the gauge is hidden in other systems in this gallery: it's a continuously changing graphic tied to scroll position, and every fact it shows (each service's name, price, and status) is written out in plain text in the card grid directly below it and in the table on the second page. The rule this table exists to enforce: a secondary button or a form field never uses `--border-strong`, because a 1.69:1 edge on a clickable thing fails the standard that exists specifically to make clickable things findable. typography: display: fontFamily: Oswald fontWeight: 600 body: fontFamily: Karla fontWeight: 400 ## Type Scale | Token | Size | Weight | Family | Line-height | Letter-spacing | Used for | |---|---|---|---|---|---|---| | `--text-display` | `clamp(2.1rem, 5vw, 3.4rem)` | 600 | Oswald | 1.08 | 0 | Homepage H1 | | `--text-page-h1` | `clamp(1.9rem, 4vw, 2.6rem)` | 600 | Oswald | 1.1 | 0 | Second-page H1 | | `--text-h2` | `1.5rem` | 600 | Oswald | 1.15 | 0 | Section headings | | `--text-h3` | `1.05rem` | 600 | Oswald | 1.2 | 0 | Service names | | `--text-body` | `1rem` | 400 | Karla | 1.6 | 0 | Paragraph copy | | `--text-small` | `0.88rem` | 400 | Karla | 1.55 | 0 | Table cells, footer, meta lines | | `--text-label` | `0.78rem` | 700 | Karla | 1 | 0.05em, uppercase | Form labels, table headers, depth captions | | `--text-control` | `0.88rem` | 700 | Karla | 1 | 0.04em, uppercase | Button labels | Oswald is condensed and uppercase by convention here, which reads as stamped or machined rather than friendly, so it's reserved for headings only and never set for more than one line at the smallest sizes. Karla carries every sentence and every control, at 400 for prose and 700 for anything functional (labels, buttons, table headers), with no third weight needed. ## Spacing Scale Base unit `8px`. Tokens in use: `--space-2: .5rem`, `--space-3: 1rem`, `--space-4: 1.5rem`, `--space-6: 2rem`, `--space-8: 3rem`, `--space-12: 4rem`. ## Shape `--radius: 4px` on cards, buttons, inputs, and the table wrap. `--radius-sm: 2px` on badges and status chips. Small and consistent rather than zero, reading as machined, chamfered metal, not the pill-shaped warmth of the candy-and-carnival systems elsewhere in this gallery, and not literally zero like a few of the moodier systems either, brass has a slight ease to its edges that a raw steel bracket wouldn't. No shadows, no glows. Depth is a 1px hairline and a background step from `--bg` to `--panel` to `--raised`. ## Iconography No icon library. The only non-text graphics are the logo mark and the hero's own cylinder cross-section (hand-built SVG). Status dots are solid circles for active states and an outlined circle for "by appointment," never a clock or bell icon standing in for a word. ## Motion Principles Scroll drops five pins onto a shared shear line: each pin is cut to a different real bitting depth, so each travels a different distance, but every one lands with its bottom edge exactly on the line. The moment a pin lands, it changes from `--border-strong` to `--accent` and the service card it represents slides in below. Once all five have landed, the whole cylinder rotates 18 degrees, the shear line itself changes from `--border-strong` to `--accent-alt`, and an "UNLOCKED" readout fades in, because that's the one moment in the sequence that deserves a second signal beyond position. One-shot reveal: nothing loops, and every service is written out in the card grid and the table regardless of scroll or script state. The trigger uses a fixed pixel-distance range (`top top` / `+=N`), not a viewport-percentage range, and progress is verified at exactly `0` at scroll position `0` so the pins never render pre-dropped on load. `N` is computed at load as the real scroll room remaining below the stage, capped at `480px`, rather than a hardcoded number. A static `+=650` first silently capped progress at `.84` on a normal desktop viewport, because the real page couldn't scroll that far. A static `+=480` fixed that but still failed on a very tall (1400px) viewport, where less total scroll room exists in the first place. A third attempt added a `160px` floor to the clamp and still failed at that same extreme, because a floor above the real measured availability is the same bug wearing a different number. The floor was removed, and one case remains genuinely unfixable by any distance value: a viewport taller than the entire document has zero scroll room, full stop. That case is treated the same as `prefers-reduced-motion`, the resolved state renders immediately and no scroll listener is attached, because a scroll-gated reveal that can structurally never receive a scroll event would otherwise hide every card permanently, which breaks this gallery's own decorate-don't-gate rule far worse than skipping the animation does. A final `16px` safety margin is subtracted from the measured distance, added after Broadside Press (built the same day, right after this system) showed that scrolling to the page's true maximum can land a few pixels short of an unpadded measurement, since layout can still settle slightly after the measurement runs, leaving progress permanently just under `1` and the cylinder never quite turning. `prefers-reduced-motion: reduce` renders every pin already set, the cylinder already turned, and every card already visible. ## Voice & Tone Plain, trade-competent, no fear-based scare copy. Keyway talks like someone who already knows the job, not like an ad convincing you that you need one. In practice: - **Primary button**: "Request service", not "Protect your family today" - **Lockout copy**: "Locked out of the house or car, no damage to the lock, most calls answered in under an hour.", not "Don't panic, help is on the way!" - **Ownership check**: "We ask for ID and proof before we touch the lock, every time, no exceptions for being in a hurry.", not "Your security is our top priority" - **Form error**: "That doesn't look like a callable number. Check it and resend.", not "Invalid input" - **Callback note**: "We call before we come, never just show up." The tell that a sentence is off-brand: it invokes fear, safety, or trust as an abstraction instead of naming the actual step being taken. ## Accessibility Baseline - **Focus ring**: `2px solid var(--accent)` at `2px` offset via `:focus-visible` on every link, button, input, and select, never removed. - **Reduced motion**: honored on the pin-drop, the cylinder rotation, and the card reveal, as described under Motion Principles. No scroll listener is attached at all when reduce is set. - **The cylinder graphic is `aria-hidden`.** It's a continuously changing illustration tied to scroll position. Nothing is communicated by it alone: every service's name, depth, price, and status is in the card grid below in plain markup at all times, and again in the second page's table. - **Tap targets**: `44px` minimum height on every button, nav link, and form control. - **Color is never the only signal**: every status pairs a dot with a word ("On call", "By appointment"), and the depth bars on the table are paired with a number, not left as color alone. - **Skip link**: the second page opens with a `position: fixed` skip link to its main content. - **Focus is never dropped**: on a successful service request, focus moves to the confirmation message, and the phone field is set `readonly` rather than `disabled`, so focus is never dumped to the document root. - **Form errors**: the phone field sets `aria-invalid`, changes its border to `--accent-alt`, and the helper text is replaced with a sentence naming the actual problem, not a generic "invalid input." - **Structure**: one `