The full-width Pro sales banner at the bottom of the emoji grid. Clicking it opens the Pro modal.
<ProPrompt> + <PromptWrapper> — STAGING build components (live on staging.emojicopy.com, not yet in this repo copy). Update ProPrompt in place: the from="inline" branch renders the strip instead of the prompt-card; the floating render is untouched.useUpsellStore — updateShowPro(true) (staging store), the same call today's card makes.<PrimaryButton variant="green"> · <Text>.upsell-strip scoped CSS — the only new styling<script setup>
import PrimaryButton from '@/buttons/PrimaryButton.vue'
import PromptWrapper from '@/components/Upsell/PromptWrapper.vue'
import { useUpsellStore } from '@/store/upsell'
const props = defineProps(['position', 'from'])
const upsell = useUpsellStore()
const open = () => upsell.updateShowPro(true)
</script>
<template>
<!-- inline (bottom of the emoji grid) — the full-width Pro sales
banner. The PrimaryButton is the semantic control; the strip's
own @click is pointer convenience only (no role/tabindex —
keyboard and AT users get the real button). -->
<div v-if="props.from === 'inline'" class="upsell-strip" @click="open()">
<div class="__intro">
<Text tag="h5" text="Get more with Pro!" />
<Text type="body/s-med" class="__sub"
text="50% off for life — annual plan, first 500 beta members only." />
</div>
<div class="__right">
<div class="__tiles" aria-hidden="true">
<span class="__tile jp-font">👑</span>
<span class="__tile jp-font">🧩</span>
<span class="__tile jp-font">📋</span>
<span class="__tile jp-font">⚡</span>
</div>
<PrimaryButton variant="green" size="md" label="Take 50% off now!"
@click.stop="open()" />
</div>
</div>
<!-- floating render (bottom-left prompt card) — UNCHANGED existing path -->
<PromptWrapper v-else :dismissed="true" :from="props.from" @clicked="open()">
<!-- …existing ProPrompt card markup, untouched… -->
</PromptWrapper>
</template>
<style scoped>
.upsell-strip {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-6);
width: 100%;
margin-top: var(--sp-7); /* the strip leads the bottom stack;
the two prompt cards follow below */
padding: var(--sp-5) var(--sp-6);
border-radius: var(--r-lg);
background: var(--metal-950); /* ALWAYS dark, both schemes —
flat black only; doodle bg is
reserved for Explore + collections */
border: 1px solid rgba(255, 255, 255, .08);
cursor: pointer;
transition: border-color .15s;
&:hover {
border-color: rgba(255, 255, 255, .18);
}
/* bare `.__elem` descendants — keeps staging's literal `__x`
class convention (like PromptWrapper's `__close` / `__message`),
NOT the full-BEM `&__elem` form (R6 pairs that with full-BEM
markup, which this template doesn't use) */
.__intro {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--sp-2);
min-width: 0;
h5 {
font-size: 2.2rem; /* card-scale take on the modal h5 */
line-height: 1.1;
letter-spacing: -.02em;
color: var(--white); /* PINNED — don't use color="white":
it maps to --c-text-white, which
flips to metal-950 in dark scheme
and vanishes on this fixed-dark
shell (the sub pins its ink the
same way) */
}
}
.__sub {
color: rgba(255, 255, 255, .64);
}
.__right {
display: flex;
align-items: center;
gap: var(--sp-6);
flex: none;
}
/* decorative emoji tiles — hand-arranged tilt, straighten on hover */
.__tiles {
display: flex;
align-items: center;
}
.__tile {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: var(--r-md);
background: rgba(255, 255, 255, .06);
border: 1px solid rgba(255, 255, 255, .08);
font-size: 2rem;
line-height: 1;
rotate: -4deg;
transition: rotate .2s, translate .2s;
+ .__tile { margin-left: -6px; }
&:nth-child(2n) { rotate: 3deg; }
&:nth-child(3) { rotate: -2deg; }
}
&:hover .__tile {
rotate: 0deg;
translate: 0 -2px;
}
/* mobile — the "stacked" layout from Section 2. To drop the tiles
on very small screens (the "compact" option), add a nested
(max-width: 480px) rule hiding .__tiles. */
@media screen and (max-width: 800px) {
flex-direction: column;
align-items: stretch;
gap: var(--sp-4);
.__right {
flex-direction: column;
align-items: stretch;
gap: var(--sp-4);
}
.__tiles {
justify-content: flex-start;
}
.__right .button {
width: 100%;
justify-content: center;
}
}
}
</style>
<ProPrompt> + <PromptWrapper> — STAGING build components (live on staging.emojicopy.com, not yet in this repo copy). Update ProPrompt in place: the from="inline" branch renders the strip instead of the prompt-card; the floating render is untouched.useUpsellStore — updateShowPro(true) (staging store), the same call today's card makes.<PrimaryButton variant="green"> · <Text>.upsell-strip scoped CSS — the only new styling<script setup>
import PrimaryButton from '@/buttons/PrimaryButton.vue'
import PromptWrapper from '@/components/Upsell/PromptWrapper.vue'
import { useUpsellStore } from '@/store/upsell'
const props = defineProps(['position', 'from'])
const upsell = useUpsellStore()
const open = () => upsell.updateShowPro(true)
</script>
<template>
<!-- inline (bottom of the emoji grid) — the full-width Pro sales
banner. The PrimaryButton is the semantic control; the strip's
own @click is pointer convenience only (no role/tabindex —
keyboard and AT users get the real button). -->
<div v-if="props.from === 'inline'" class="upsell-strip" @click="open()">
<div class="__intro">
<Text tag="h5" text="Get more with Pro!" />
<Text type="body/s-med" class="__sub"
text="50% off for life — annual plan, first 500 beta members only." />
</div>
<div class="__right">
<div class="__tiles" aria-hidden="true">
<span class="__tile jp-font">👑</span>
<span class="__tile jp-font">🧩</span>
<span class="__tile jp-font">📋</span>
<span class="__tile jp-font">⚡</span>
</div>
<PrimaryButton variant="green" size="md" label="Take 50% off now!"
@click.stop="open()" />
</div>
</div>
<!-- floating render (bottom-left prompt card) — UNCHANGED existing path -->
<PromptWrapper v-else :dismissed="true" :from="props.from" @clicked="open()">
<!-- …existing ProPrompt card markup, untouched… -->
</PromptWrapper>
</template>
<style scoped>
.upsell-strip {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-6);
width: 100%;
margin-top: var(--sp-7); /* the strip leads the bottom stack;
the two prompt cards follow below */
padding: var(--sp-5) var(--sp-6);
border-radius: var(--r-lg);
background: var(--metal-950); /* ALWAYS dark, both schemes —
flat black only; doodle bg is
reserved for Explore + collections */
border: 1px solid rgba(255, 255, 255, .08);
cursor: pointer;
transition: border-color .15s;
&:hover {
border-color: rgba(255, 255, 255, .18);
}
/* bare `.__elem` descendants — keeps staging's literal `__x`
class convention (like PromptWrapper's `__close` / `__message`),
NOT the full-BEM `&__elem` form (R6 pairs that with full-BEM
markup, which this template doesn't use) */
.__intro {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--sp-2);
min-width: 0;
h5 {
font-size: 2.2rem; /* card-scale take on the modal h5 */
line-height: 1.1;
letter-spacing: -.02em;
color: var(--white); /* PINNED — don't use color="white":
it maps to --c-text-white, which
flips to metal-950 in dark scheme
and vanishes on this fixed-dark
shell (the sub pins its ink the
same way) */
}
}
.__sub {
color: rgba(255, 255, 255, .64);
}
.__right {
display: flex;
align-items: center;
gap: var(--sp-6);
flex: none;
}
/* decorative emoji tiles — hand-arranged tilt, straighten on hover */
.__tiles {
display: flex;
align-items: center;
}
.__tile {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: var(--r-md);
background: rgba(255, 255, 255, .06);
border: 1px solid rgba(255, 255, 255, .08);
font-size: 2rem;
line-height: 1;
rotate: -4deg;
transition: rotate .2s, translate .2s;
+ .__tile { margin-left: -6px; }
&:nth-child(2n) { rotate: 3deg; }
&:nth-child(3) { rotate: -2deg; }
}
&:hover .__tile {
rotate: 0deg;
translate: 0 -2px;
}
/* mobile — the "stacked" layout from Section 2. To drop the tiles
on very small screens (the "compact" option), add a nested
(max-width: 480px) rule hiding .__tiles. */
@media screen and (max-width: 800px) {
flex-direction: column;
align-items: stretch;
gap: var(--sp-4);
.__right {
flex-direction: column;
align-items: stretch;
gap: var(--sp-4);
}
.__tiles {
justify-content: flex-start;
}
.__right .button {
width: 100%;
justify-content: center;
}
}
}
</style>
<ProPrompt> + <PromptWrapper> — STAGING build components (live on staging.emojicopy.com, not yet in this repo copy). Update ProPrompt in place: the from="inline" branch renders the strip instead of the prompt-card; the floating render is untouched.useUpsellStore — updateShowPro(true) (staging store), the same call today's card makes.<PrimaryButton variant="green"> · <Text>.upsell-strip scoped CSS — the only new styling<script setup>
import PrimaryButton from '@/buttons/PrimaryButton.vue'
import PromptWrapper from '@/components/Upsell/PromptWrapper.vue'
import { useUpsellStore } from '@/store/upsell'
const props = defineProps(['position', 'from'])
const upsell = useUpsellStore()
const open = () => upsell.updateShowPro(true)
</script>
<template>
<!-- inline (bottom of the emoji grid) — the full-width Pro sales
banner. The PrimaryButton is the semantic control; the strip's
own @click is pointer convenience only (no role/tabindex —
keyboard and AT users get the real button). -->
<div v-if="props.from === 'inline'" class="upsell-strip" @click="open()">
<div class="__intro">
<Text tag="h5" text="Get more with Pro!" />
<Text type="body/s-med" class="__sub"
text="50% off for life — annual plan, first 500 beta members only." />
</div>
<div class="__right">
<div class="__tiles" aria-hidden="true">
<span class="__tile jp-font">👑</span>
<span class="__tile jp-font">🧩</span>
<span class="__tile jp-font">📋</span>
<span class="__tile jp-font">⚡</span>
</div>
<PrimaryButton variant="green" size="md" label="Take 50% off now!"
@click.stop="open()" />
</div>
</div>
<!-- floating render (bottom-left prompt card) — UNCHANGED existing path -->
<PromptWrapper v-else :dismissed="true" :from="props.from" @clicked="open()">
<!-- …existing ProPrompt card markup, untouched… -->
</PromptWrapper>
</template>
<style scoped>
.upsell-strip {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-6);
width: 100%;
margin-top: var(--sp-7); /* the strip leads the bottom stack;
the two prompt cards follow below */
padding: var(--sp-5) var(--sp-6);
border-radius: var(--r-lg);
background: var(--metal-950); /* ALWAYS dark, both schemes —
flat black only; doodle bg is
reserved for Explore + collections */
border: 1px solid rgba(255, 255, 255, .08);
cursor: pointer;
transition: border-color .15s;
&:hover {
border-color: rgba(255, 255, 255, .18);
}
/* bare `.__elem` descendants — keeps staging's literal `__x`
class convention (like PromptWrapper's `__close` / `__message`),
NOT the full-BEM `&__elem` form (R6 pairs that with full-BEM
markup, which this template doesn't use) */
.__intro {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--sp-2);
min-width: 0;
h5 {
font-size: 2.2rem; /* card-scale take on the modal h5 */
line-height: 1.1;
letter-spacing: -.02em;
color: var(--white); /* PINNED — don't use color="white":
it maps to --c-text-white, which
flips to metal-950 in dark scheme
and vanishes on this fixed-dark
shell (the sub pins its ink the
same way) */
}
}
.__sub {
color: rgba(255, 255, 255, .64);
}
.__right {
display: flex;
align-items: center;
gap: var(--sp-6);
flex: none;
}
/* decorative emoji tiles — hand-arranged tilt, straighten on hover */
.__tiles {
display: flex;
align-items: center;
}
.__tile {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: var(--r-md);
background: rgba(255, 255, 255, .06);
border: 1px solid rgba(255, 255, 255, .08);
font-size: 2rem;
line-height: 1;
rotate: -4deg;
transition: rotate .2s, translate .2s;
+ .__tile { margin-left: -6px; }
&:nth-child(2n) { rotate: 3deg; }
&:nth-child(3) { rotate: -2deg; }
}
&:hover .__tile {
rotate: 0deg;
translate: 0 -2px;
}
/* mobile — the "stacked" layout from Section 2. To drop the tiles
on very small screens (the "compact" option), add a nested
(max-width: 480px) rule hiding .__tiles. */
@media screen and (max-width: 800px) {
flex-direction: column;
align-items: stretch;
gap: var(--sp-4);
.__right {
flex-direction: column;
align-items: stretch;
gap: var(--sp-4);
}
.__tiles {
justify-content: flex-start;
}
.__right .button {
width: 100%;
justify-content: center;
}
}
}
</style>
@media screen and (max-width: 480px) {
.__tiles {
display: none;
}
}