Guidelines

Calm-First CTA Guideline

**Version: v1** **Applies to: any consumer template or page declaring a page-hero CTA via `GyPageHero` or any canon page-header primitive.**

v1

Calm-First CTA Guideline

Version: v1 Applies to: any consumer template or page declaring a page-hero CTA via GyPageHero or any canon page-header primitive.

This guideline extends the calm-first principle (warp#2206) with a CTA-specific rule. PRs touching page-hero CTAs cite Guideline: calm-first-cta@v1 in the PR body so persona reviewers (ADR-115) verify against this exact version.

Rule

At rest — i.e. on a steady-state page that the operator visits to look at content, not to recover from an error — page-hero CTAs render with variant="secondary". The variant="primary" fill is reserved for moments of attention: calm-first empty-slot CTAs ("you have no projects yet, create one"), error-recovery actions ("retry"), and decision prompts ("confirm deploy").

The rule applies to:

  • The variant prop on GyButton (or equivalent canon button primitive) when used as the first/only CTA in a GyPageHero slot.
  • Any <button> styled with class="btn--primary" or class="cta-primary" within a page-hero region.
  • Any element in the page-hero CTA position whose visual weight matches "primary fill" — solid dark/brand fill, high-contrast text, hero-prominent padding.

The rule does NOT apply to:

  • Empty-state slots (empty slot on calm-first templates like DashboardTemplate from warp#3616) — primary fill is correct there; the slot's whole job IS calling for action.
  • Error-state surfaces (404, deploy-failed pages) — primary fill on the recovery action is correct; the surface IS a moment of attention.
  • Modal-confirm dialogs and decision-prompt overlays — primary fill is correct there; the user is being asked to make a decision now.

Why

Today's empirical evidence: today's /projects page hero (ai-frontend) ships a dark "New project" variant="primary" button as the loudest visual element on a steady-state catalog page. Operators visiting /projects to browse the list see the primary CTA's dark fill before they see any of the actual project rows. The CTA's weight trains operators to ignore visual weight as a signal — when something genuinely urgent appears later (a calm-first empty slot prompt, an error-state retry button), the operator's eye has nowhere distinctive to land.

Sister evidence: V96 today (warp#3616) shipped DashboardTemplate with a calm-first empty slot that takes over the body region when the dashboard has nothing to show. The empty-slot CTA there ("create your first dashboard") is correctly variant="primary" — that's a moment-of-attention surface. The operator is on a dashboard page expecting to see data and finds none; the primary CTA is the right next-action.

When EVERY page hero ships primary CTAs, the primary/secondary distinction collapses into "the only style we use". The calm-first principle (warp#2206) requires the distinction to be meaningful — primary fill earns attention precisely because most surfaces don't use it. Three failure modes follow when the distinction collapses:

  1. Visual hierarchy noise. The primary fill on a steady-state hero competes with content for attention. Operators slow down to filter the CTA out before reading the content.
  2. Empty-state under-emphasis. When the calm-first empty slot finally fires (a dashboard with no data, a project list with no projects), its primary CTA blends into the visual baseline of "every page has a primary CTA" and reads as cosmetic rather than the urgent next-action it's meant to be.
  3. Error-recovery under-emphasis. Same shape as (2) — the error-page "retry" button no longer stands out because operators have learned to ignore primary fills.

The fix: default page-hero CTAs to variant="secondary" (token-driven outlined or low-fill style). Reserve variant="primary" for the three surfaces named above. The visual hierarchy then carries information: a primary fill means "something needs your attention here", not "this page has a header".

Examples — passes

Steady-state catalog page hero — secondary CTA:

<GyPageHero title="Projects" subtitle="Your active and archived projects.">
    {#snippet actions()}
        <GyButton variant="secondary" href="/projects/new">
            New project
        </GyButton>
    {/snippet}
</GyPageHero>

Calm-first empty slot — primary CTA correct here:

<DashboardTemplate {state}>
    {#snippet empty()}
        <GyEmptyState heading="No dashboards yet" body="Create one to start.">
            <GyButton variant="primary" href="/dashboards/new">
                Create your first dashboard
            </GyButton>
        </GyEmptyState>
    {/snippet}
</DashboardTemplate>

Error-recovery surface — primary CTA correct here:

<!-- src/routes/+error.svelte -->
<GyPageHero title="Something went wrong" subtitle={$page.error?.message}>
    {#snippet actions()}
        <GyButton variant="primary" onclick={() => location.reload()}>
            Retry
        </GyButton>
    {/snippet}
</GyPageHero>

Multi-CTA hero — primary reserved for the rare urgent action, secondary for the steady-state one:

<!-- Only correct when the page genuinely has both a steady-state action
     and an attention-requiring one (e.g. unread-count drives 'review now') -->
<GyPageHero title="Inbox">
    {#snippet actions()}
        {#if unreadCount > 0}
            <GyButton variant="primary" href="/inbox/review">
                Review {unreadCount} new
            </GyButton>
        {/if}
        <GyButton variant="secondary" href="/inbox/settings">
            Settings
        </GyButton>
    {/snippet}
</GyPageHero>

Examples — fails

Steady-state hero shipping primary CTA (today's lived failure):

<!-- FAILS — /projects is a steady-state catalog; primary CTA at rest
     trains operators to ignore visual weight -->
<GyPageHero title="Projects" subtitle="Your active and archived projects.">
    {#snippet actions()}
        <GyButton variant="primary" href="/projects/new">
            New project
        </GyButton>
    {/snippet}
</GyPageHero>

Custom-class CTA matching primary visual weight:

<!-- FAILS — bypasses the variant prop but ships the same visual weight -->
<GyPageHero title="Settings">
    {#snippet actions()}
        <button class="cta-loud" onclick={save}>Save</button>
    {/snippet}
</GyPageHero>

<style>
    .cta-loud {
        background: var(--text);
        color: var(--bg);
        padding: var(--space-3) var(--space-5);
    }
</style>

Multi-primary CTA hero (no priority order):

<!-- FAILS — two primaries cancel each other out; operator has no signal -->
<GyPageHero title="Project">
    {#snippet actions()}
        <GyButton variant="primary" href="/deploy">Deploy</GyButton>
        <GyButton variant="primary" href="/settings">Settings</GyButton>
    {/snippet}
</GyPageHero>

Path forward when canon doesn't have what you need

Path A — in-PR (warp#3100)

If the canon GyButton primitive doesn't expose a variant="secondary" that fits your design at the page-hero scale, that's canon-tier work. Author the variant on the canon primitive in the same PR — most likely as a new value in the variant enum with token-driven fill / border / text-color rules. Update the canon documentation page for GyButton with the new variant's visual contract. Register the variant in canon-manifest.yaml if appropriate.

Path B — sister ticket

If the page-hero genuinely needs a distinct CTA primitive that GyButton shouldn't carry (e.g. a hero-only "stamp" affordance with a different shape grammar), file a sister ticket for the new primitive. Tag the current PR blocked-by:warp#<sister> and ship the current hero with the closest existing variant="secondary" in the meantime, with a /* canon-extension: warp#NNN */ marker if any layout CSS sits in the consumer file (per canon-extension-discipline).

Audited override

gyrum-review-pr --skip-calm-first-cta "<reason ≥ 30 chars>" — logged to ~/.gyrum/admin-overrides.log. Use ONLY when the surface is genuinely attention-requiring (onboarding flows where the page IS the call-to-action; one-off promotional landing pages; security-prompt pages). The reason MUST name (a) why this surface is attention-requiring rather than steady-state, (b) why the calm-first separation costs more than it earns here, and (c) the audit-log line referring to the operator's acknowledgement.

PR body citation

Required line in the PR body when changing page-hero CTAs:

Guideline: calm-first-cta@v1

Revision process

This document is versioned. Updates ride gyrum-revise-guideline calm-first-cta (separate devtools wrapper, separate ticket).

References