--- 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 `

` per page, headings in order, `nav` / `main` / `footer` landmarks, a real `` on the service table, `scope="col"` on every header cell, `aria-current="page"` on the active nav link. - **Controls carry state**: the phone field's helper is tied to the input via `aria-describedby`, and the confirmation region is `role="status"`. ## Components ### Navigation Single line, `64px`, `--panel` with a `--border` bottom hairline, sticky. Left: the mark plus wordmark lockup. Right: a "Services" link and exactly one primary button. No hamburger, a two-page trade site has nothing worth collapsing. Below 480px the first nav link drops, the primary button stays. ### Buttons Two variants, `4px` radius, `44px` minimum height: - **Primary**: `--accent` fill, white label, brightness lift on hover. Once per view, for the real action ("Request service", "Send request"). - **Secondary**: transparent, `1px solid --border-ui`, `--ink` label, edge and label both going `--accent` on hover. Both define `:hover`, `:active` (`translateY(1px)`, a press), `:disabled` (`opacity: .5`, `pointer-events: none`), and `:focus-visible`. ### The cylinder (signature) Documented in full under Motion Principles. Structurally: a static portrait housing, five channel wells, a dashed shear line, and five pins of five different heights, each a `` moved by a scroll-driven `translateY` and switching fill on landing. ### Service card A flat panel: `--panel` background, `1px solid --border`, `3px` `--accent`-adjacent top rule in `--border-strong` (until the underlying pin is set, the rest of the card's content is what actually reveals). Holds a depth label, the service name, a real description, the price, and a status badge. Present in the DOM at all times, decorated by the reveal, never gated by it. ### Depth table (second page) `--raised` header row in `--text-label`, `--panel` body rows, one `--border` rule between rows, `--raised` on row hover, a `` naming the sort order. A depth cell pairs a filled bar with the number, so depth reads as both a proportion and a digit. Below 700px the table restacks into one bordered block per service, each cell labelled from `data-label`, never hidden behind horizontal scroll. ### Status states - **On call / Same day**: filled brass dot plus the word. - **By appointment**: outlined dim dot plus the word. ### Notice A `--panel` block with a `3px` `--accent-alt` top rule, carrying the ownership-verification policy, the one place on the site the alarm color appears outside the unlocked state. Square, not rounded, a bolted plate rather than another card in the grid: the mechanical detector's `border-accent-on-rounded` rule flagged the original version, which paired this same thick top rule with `--radius`, and a plate reads more honestly than shaving the border down to fit a radius it was never going to sit well against. ### Form Label above control in `--text-label`. Select and input share `--bg` fill, `1px solid --border-ui`, `4px` radius, `44px` height, `--raised` fill plus an accent border on focus. Helper text sits below at a reserved minimum height so validating doesn't shift the layout. An empty field and a malformed number get different sentences. On success the helper goes brass, restates the service and number back to the visitor, and focus moves to the confirmation before the field is set read-only. ### Footer Two-part row: an honest note that the shop is fictional, and a link list back to the other page and the gallery. No invented legal text. ## Do's and Don'ts ### Do - Let every pin land at a different point but on the same line. Depth varies, the standard doesn't. - State a job's price and typical time together, every time. - Order services by real depth, quickest first, longest last. - Use `--accent-alt` for exactly one thing: the moment of change (unlocked, an error), never as decoration. - Keep the cylinder `aria-hidden` and write every fact it shows in plain text nearby. ### Don't - Don't use a stock photo of a locksmith, a giant keyring, or a hand on a doorknob. - Don't write fear-based security copy. Name the service and the price instead. - Don't use a cartoon padlock mascot or scatter key-shaped icons as decoration. - Don't replay the pin-drop on every scroll pass, it plays once, like a real pick. - Don't round anything into a pill. This trade reads machined, not playful. - Don't put `--border-strong` on anything a person can click, type in, or drag, that's what `--border-ui` is for. - Don't invent a green "all clear" status, availability is a word, not a color. ### Deliberate exception The static-HTML pass of the mechanical detector still reports `border-accent-on-rounded` on the notice's `border-top: 3px solid` declaration, because its regex engine matches the property text alone and has no visibility into the cascade, it can't see that `.notice` was changed to `border-radius: 0`. The condition the rule exists to catch, a thick accent border sitting against a rounded corner, is gone: the element is square. This was confirmed by reading the resolved CSS rule directly, not assumed. ## Known Constraints - Two routes exist (home, services). There is no technician-profiles page, no live scheduling, and no authenticated state. "Request service" is a static form that produces a client-side confirmation only, this is a concept demo. - The cylinder holds exactly five pins because the shop offers exactly five services. A sixth service would need a sixth channel, not a re-labeled existing one. - Responsive behavior is two breakpoints (900px, 700px, 480px) plus fluid type. There is no tablet-specific layout beyond the table's restack point. - The form's phone validation is a plain-text pattern check with no country-code handling, sufficient for a concept demo, not a production phone validator. ## Process Built through the impeccable-audited anti-slop workflow: never-list written before any HTML, page structure derived from the signature interaction (five pins, one shear line) rather than a template, a dedicated `--border-ui` token added specifically because the first pass had put `--border-strong` under interactive controls where it measured under 2:1, and the mechanical detector run clean before this system was considered done.