MolinoPro

prompts-roadmap

Master Codebase Guidebook
Markdown + HTML Dev-Docs Renderer - Frontend Client Module

Default Index
Open README.md
Root: README.mdarchive
Milestones

//one by ome prompt based spec definition..

Yes — and this is a good moment to align instead of duplicating. Short answer: do not introduce a parallel “StudyCard” domain. You already have a mature Card system. We should project Skills into it, not fork it.

Below is a clean, minimal alignment plan that respects your existing models and avoids schema sprawl.

  1. What you already have (and should reuse)

You already have a canonical card system:

Existing, stable truth • Chapter • PromptCard • Palette • Highlight

This system already supports: • ordering • grouping • visual semantics (palette) • rendering • study / learning / reference use cases

This is not just dev-only — it is already a knowledge card engine.

Conclusion: 👉 Skills should feed this system, not replace it.

  1. What the new Skill layer actually is

Your new layer introduces runtime / commercial / experiential structure, not knowledge rendering.

Conceptually:

Layer Role MicroSkill Capability / offering / competence MicroSession Delivery instance / format SessionStep Operational steps / pedagogy PromptCard Knowledge atom / explanation

So the relationship is not parallel, it is derivative.

A SessionStep can generate or reference one or more PromptCards.

  1. The key design decision (important)

You have two valid options. Only one scales cleanly.

✅ Option A (recommended): Skills project into Cards

Do not create a new “StudyCard” model. Instead: • Keep PromptCard as-is • Add a thin link layer from Skills → Cards

Minimal new model

model SkillCardLink { id String @id @default(cuid()) skillId String sessionId String? stepId String? cardId String order Int @default(0) purpose String? // "primary", "supplement", "marketing"

card PromptCard @relation(fields: [cardId], references: [id], onDelete: Cascade)

@@index([skillId]) @@index([sessionId]) @@index([stepId]) }

Why this is correct • No duplication of content • Cards remain reusable across: • dev study • coaching • posters • PDFs • Skills become curated views over cards

❌ Option B (not recommended): duplicate card models

This would mean: • SkillCard • SessionCard • MarketingCard

You already know where that leads: schema explosion, rendering forks, cognitive debt.

  1. How /skills/[skillId]/cards should really work

Instead of rendering ad-hoc StudyCard, the page should: 1. Resolve the Skill 2. Resolve linked PromptCards 3. Render existing Card UI

Revised mental model for the page

Skill └─ Session └─ Steps └─ SkillCardLink └─ PromptCard (rendered)

  1. Revised page.tsx (aligned with your system)

This is what /skills/[skillId]/cards/page.tsx should evolve into:

// READ-ONLY ORCHESTRATION

const cards = await prisma.skillCardLink.findMany({ where: { skillId: params.skillId }, orderBy: { order: "asc" }, include: { card: true, }, });

Then render using your existing cards renderer (the same one used in /cards/[slug]).

No new card UI. No new semantics. No drift.

  1. Where SessionStep fits now

SessionStep should not own long-form content.

Instead:

model SessionStep { id String @id @default(cuid()) sessionId String position Int title String

// optional short guidance only cue String? // what to do timingMin Int?

// knowledge lives elsewhere }

Long-form explanations live in PromptCard.content.

  1. Marketing & commercial value (you asked explicitly)

This structure lets you: • reuse the same cards for: • coaching UI • public landing pages • printed posters • PDF exports • tag cards by purpose = "marketing" • selectively expose cards publicly without leaking internal logic

You already have: • isPublic • visibility • Palette • Space / Section

This fits cleanly.

  1. Clear recommendation

Do this, in order: 1. ❌ Do NOT add a new StudyCard model 2. ✅ Add SkillCardLink 3. ✅ Strip long text out of SessionStep 4. ✅ Make /skills/[skillId]/cards a projection over PromptCard 5. ✅ Reuse the existing Cards UI

  1. Next concrete step (I recommend)

If you want, next I can: • propose the exact Prisma migration • refactor your current SkillCardsPage to use PromptCard • or define the poster renderer that consumes the same cards

Just tell me which one you want next.

Below is a single, clear, execution-grade path that turns everything you’ve written so far into a paced implementation plan you can actually follow week by week.

This is not new theory. This is a compressed roadmap that aligns: • Skill-first MLV • Prisma + Next.js reality • Physical postcards/posters in a local radius • Online reuse (landing pages, socials, in-app) • One-to-one sessions (in person + online) • Zero over-engineering up front

You can treat this as your operational playbook for the next 6–10 weeks.

PHASE 0 — LOCK THE INTENT (DO THIS ONCE)

Outcome: no more conceptual drift.

Rules (already agreed, now operational): 1. Micro-Skill is the root 2. Sessions execute skills (90′) 3. Steps contain knowledge 4. Cards/Posters/PDFs only render steps 5. Documents are instruments, not products 6. Trips are applied programs of skills

👉 Nothing new gets built unless it respects this chain.

PHASE 1 — DATA & CODE SPINE (DEV FIRST, 1–2 WEEKS)

Step 1.1 — Commit the Skill-First Prisma Migration

(you already have it) • Enums • MicroSkill • MicroSession • SessionStep • Asset • Program

Stop here until: • migration runs • Prisma client regenerates • seed script runs cleanly

Step 1.2 — Seed ONLY 3 Skills (Not 30)

Use real skills you can sell immediately, not the full catalog.

Recommended initial trio: 1. iPad Creative Workflow 2. Google Workspace for Professionals 3. Andalusia Cultural Journey – Córdoba → Granada

Each seeded with: • 1 online session • 1 in-person session • 4–6 canonical steps

👉 This is your entire universe for the first launch.

Step 1.3 — Wire MicroSkill ↔ Document2Data (Minimal)

Goal: A Folio document = authoring surface for one MicroSkill.

Do NOT: • build a generic CMS • build editors for everything

Do: • store microSkillId on the document • render SessionSteps into StudyCards via StudyCard.tsx • reuse existing Folio pagination & rendering

Outcome: • Skills can be authored like documents • Cards are rendered, not hand-designed

PHASE 2 — FIRST CARD → FIRST OFFER (2 WEEKS)

Step 2.1 — Finalise StudyCard as Atomic Unit

You already have: • StudyCard.tsx • Print-safe CSS

Lock this. No variants yet.

This card is: • screen • print • PDF • poster content • postcard content

Step 2.2 — Create ONE Poster Wrapper

Poster = Card + Hero + QR

Minimal component: • Skill title • One key step headline • “90′ One-to-One Session” • QR → /skills/[slug]

No pricing logic. No booking logic yet.

This lets you: • print posters • screenshot for socials • reuse in app

Step 2.3 — Create ONE Skill Landing Page

Route:

/skills/[skillSlug]

Content: • Skill description • Steps rendered as StudyCards • Session options: • Online • In-person (Granada) • One CTA: → “Request a Session”

This page is: • what QR codes point to • what social posts link to • what postcards link to

PHASE 3 — PHYSICAL DISTRIBUTION (LOW TECH, HIGH SIGNAL)

Step 3.1 — Define a Micro-Radius

Choose: • 1 coworking space • 2 cafés • 1 library / cultural venue

Walking distance from where you can host sessions.

No expansion yet.

Step 3.2 — Print 3 Assets Only

For EACH of the 3 skills: 1. Poster • Window / board • QR to skill page 2. Postcard • Same QR • Collectable • One sentence + title 3. Study Card (single step) • Not full deck • Teaser only

👉 Do NOT print full decks yet.

Step 3.3 — Manual Fulfilment (Important)

For the first 5–10 sessions: • booking can be manual • payment can be manual • calendar can be manual

Why: You are validating signal, not scaling.

PHASE 4 — ONLINE MIRROR (REUSE, NOT NEW WORK)

Everything physical now becomes digital without new content.

Step 4.1 — Social Activation

Each poster/postcard becomes: • Instagram post • Story • LinkedIn post • X thread

All link to:

/skills/[skillSlug]

No funnels yet. No email sequences yet.

Step 4.2 — In-App Reuse

Inside your app: • Skills list page • Same StudyCard components • Same poster wrapper as hero

Your app is now: • a catalog of skills • not a “tool” people need to understand

PHASE 5 — CONVERSION HARDENING (AFTER FIRST SALES)

Only after you’ve sold sessions.

Step 5.1 — Booking & Payment

Add: • booking slots per MicroSession • Stripe link or simple checkout • confirmation email

Do NOT add subscriptions yet.

Step 5.2 — Guidebook PDF (Upsell)

For 1 skill only: • generate Steps Guidebook PDF • priced 120–200€

This is not mandatory to launch. It’s leverage once demand exists.

PHASE 6 — TRIPS AS PROGRAMS (AFTER SKILLS PROVE)

Step 6.1 — Convert Trip into Program

Your Córdoba → Granada trip becomes: • Program (type: TRIP) • composed of MicroSessions • each day = steps

Same cards. Same renderer. Same posters.

Now: • travellers • guides • trainees

all consume the same system.

PHASE 7 — SCALE BY DUPLICATION, NOT COMPLEXITY

When ready: • add new skills • add new locations • onboard collaborators via Programs • reuse posters/cards system

No redesign. No new platform.

ONE-PAGE SUMMARY (FOR YOU)

Order of work (non-negotiable): 1. Prisma migration ✅ 2. Seed 3 real skills 3. Skill ↔ Document wiring 4. Lock StudyCard renderer 5. One poster wrapper 6. One skill landing page 7. Physical postcards/posters (local) 8. Manual sessions 9. Then automate

If you follow this order: • you ship • you sell • you don’t burn time

Next decision (pick ONE) 1. Skill ↔ Document wiring (exact code) 2. Poster / Postcard wrapper components 3. Skill landing page layout 4. Booking + payment minimal flow

Say the number.

Below is the exact, minimal, copy-pastable wiring for MicroSkill ↔ Document2Data, aligned with your locked Folio architecture and without importing any of the AppScript assumptions directly.

I will also explicitly map how your poster AppScript logic conceptually translates into the Next.js / Folio world, so nothing you built mentally is lost — only relocated correctly.

This answer is strictly Step 1. Poster comes after this is working.

STEP 1 — MICROSKILL ↔ DOCUMENT WIRING (EXACT CODE)

  1. What “wiring” means (precise)

A Document is the authoring + rendering instrument A MicroSkill is the knowledge root A Document may represent exactly one MicroSkill

So the wiring is: • Document stores microSkillId • Document2Data renders SessionSteps • Folio stays unchanged

No duplication. No CMS.

  1. Prisma: minimal addition to existing Document model

You already have Document / ProjectDocument (or similar).

Add one nullable foreign key.

model Document { id String @id @default(cuid()) title String document2Data Json

// 🔗 NEW — optional skill binding microSkillId String? microSkill MicroSkill? @relation(fields: [microSkillId], references: [id])

createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }

Why nullable? • Documents can still exist without skills • Skill documents are explicit, not forced

👉 migrate this first.

  1. Server action: bind a MicroSkill to a Document

Authority: /documents/actions/attachSkillToDocument.ts

"use server";

import {prisma} from "@/lib/prisma"; import { revalidatePath } from "next/cache";

export async function attachSkillToDocument( documentId: string, microSkillId: string ) { await prisma.document.update({ where: { id: documentId }, data: { microSkillId }, });

revalidatePath(/documents/${documentId}); }

This is the only mutation. No implicit creation. No side effects.

  1. Server fetch: document + skill + steps (read-only)

Where: your server page loader (example: app/documents/[id]/page.tsx)

import {prisma} from "@/lib/prisma";

export async function getDocumentWithSkill(documentId: string) { return prisma.document.findUnique({ where: { id: documentId }, include: { microSkill: { include: { sessions: { include: { steps: { orderBy: { position: "asc" } }, }, }, }, }, }, }); }

Important: • Read-only • No Prisma in components • No transformation here

  1. Document2Data → Skill projection (pure)

5.1 Add a SkillSection to Document2Data

This is not a new editor. It’s a renderable section.

// document/types/document2data.ts

export type SkillSection = { type: "skill"; skillId: string; };

In Document2Data.sections[], this becomes:

{ "type": "skill", "skillId": "ckxy..." }

This is analogous to how you already reference Trips, LineItems, etc.

  1. Render SkillSection (page-1 only)

File: app/documents/components/folio/lib/renderBlock.tsx

Add ONE case.

import { StudyCard } from "@/components/study-card/StudyCard";

function renderSkillSection( skill: any // already fetched server-side ) { if (!skill) return null;

const session = skill.sessions[0]; // first canonical session if (!session) return null;

return ( <section> {session.steps.map((step) => ( <StudyCard key={step.id} skillTitle={skill.title} stepTitle={step.title} stepBody={step.body} meta={{ duration: "90 min", delivery: "Online / Granada", }} /> ))} </section> ); }

Then in your existing switch:

case "skill": return renderSkillSection(viewModel.microSkill);

Key point: Pagination does not care. It paginates rendered output exactly like text blocks.

  1. Where the Skill data comes from (VM layer)

In your server page:

const doc = await getDocumentWithSkill(params.id);

const viewModel = { document: doc, microSkill: doc.microSkill, };

Pass viewModel into PaperStack.

No context mutation. No client fetching.

  1. Why this matches your AppScript mental model

Your AppScript does this:

AppScript concept New system equivalent Spreadsheet row MicroSkill Doc append block SkillSection Poster sections SessionSteps GPT enhancement later (asset generation) Google Doc Folio Document Slides template Poster renderer

Crucial difference: You no longer append text. You render structured knowledge.

This is why everything becomes reusable.

  1. What we explicitly did NOT do (important)

❌ No poster yet ❌ No AI enhancement ❌ No Slides logic ❌ No new editors ❌ No client fetches ❌ No second document system

This is deliberate.

VERIFICATION CHECKLIST (DO THIS BEFORE MOVING ON)

You are done with Step 1 when: • Document has microSkillId • Skill steps render as StudyCards inside document • Pagination still works • PDF export renders identical output • No client-side Prisma usage • No duplicated content

NEXT STEP (NOW SAFE)

Step 2 — Poster / Postcard rendering, directly inspired by your AppScript, but now: • Stateless • Deterministic • Reusing SessionStep[] • No document mutation

👉 Say “Poster next” and I will translate your AppScript logic into a clean Poster renderer + Asset generator, one-to-one.

Below is a precise, non-destructive integration plan that answers exactly: • where Skills should plug into your current app • what stays (unchanged) • what becomes a container • what gets generalized • what new folder (if any) is justified

No rewrites. No philosophical drift.

TL;DR (decision first)

Do NOT create a new app or parallel system. Do NOT replace study-guide or cards.

👉 Promote them into containers and bind them to MicroSkill.

Your instinct is correct:

“generalize them and make them containers”

That is the cleanest move.

  1. What you already have (important recognition)

You already built 90% of a Skill system, just without calling it that.

Current primitives (already correct)

Existing thing What it really is chapters.tsx Skill curriculum outline cards.tsx Study Cards / Steps StudyShell Skill container UI /study-guide/* Skill authoring + explanation surface /cards/* Atomic step renderer

This is not demo content. It is a MicroSkill: “Next.js + Prisma Developer (Entity-First)”.

So the move is promotion, not replacement.

  1. Canonical decision: ONE model, TWO sources (for now)

We introduce MicroSkill as authority, but we allow static skills to coexist.

Rule (LOCK THIS):

A MicroSkill can be backed by: • DB (Prisma) → dynamic / business skills • Static registry → internal / reference skills

Same UI. Same shell. Same cards.

  1. Exact insertion point (answering your core question)

✅ INSERT HERE (do not fight this):

// app/study-guide/chapters.tsx

This file becomes a Skill Registry Adapter, not “chapters”.

3.1 Rename mentally (not physically yet)

You keep the file, but conceptually it becomes:

StudyGuideSkillAdapter

It adapts MicroSkill → StudyShell format.

3.2 Introduce ONE discriminant type

Add this without breaking anything:

export type SkillSource = | { kind: "static"; skillKey: string } | { kind: "db"; skillId: string };

Then extend ChapterContent minimally:

export type ChapterContent = { slug: string; title: string; summary: string; sections: Section[];

// 🔗 NEW (non-breaking) skill?: SkillSource; };

Your existing chapters become:

{ slug: "00-layout", title: "00 · Root Layout", summary: "...", skill: { kind: "static", skillKey: "nextjs-entity-first" }, sections: [...] }

Nothing breaks. Nothing rerenders differently. You’ve just attached intent.

  1. Cards = Steps (formalize this)

You already have:

export type CardContent = { slug: string; title: string; summary: string; chapterSlug: string; sections: Section[]; };

This already is a SessionStep.

Add ONE optional binding:

export type CardContent = { ... skillStepKey?: string; // stable identifier };

Example:

{ slug: "01-server-action", title: "Card · Server Page", skillStepKey: "server-page-read-only", chapterSlug: "01-server-action", ... }

Now: • Static cards = canonical step definitions • DB-backed skills can reuse this renderer

  1. Where Prisma-backed skills enter (cleanly)

NEW folder (justified, minimal):

app/skills/ page.tsx // list skills (DB) [skillId]/ page.tsx // skill overview cards/ page.tsx // render steps as cards

This does not replace /study-guide.

It sits alongside it.

5.1 Skill page uses the SAME shell

// app/skills/[skillId]/layout.tsx import { StudyShell } from "@/app/study-guide/components/layouts/StudyShell";

export default function SkillLayout({ children }) { return ( <StudyShell heading="Skill" items={[]} // later: sessions / steps basePath="/skills" > {children} </StudyShell> ); }

Same shell. Different source.

  1. Poster & assets (important positioning)

Your AppScript posters map exactly to this layer:

MicroSkill → MicroSession → SessionStep[] → Asset(type=POSTER)

In your current app: • cards = steps • StudyCard = render primitive • Poster is just a different renderer

⚠️ Do NOT insert poster logic into study-guide Poster belongs to:

app/assets/ poster/ PosterRenderer.tsx

It consumes SessionStep[] (DB or static).

  1. What NOT to do (explicitly)

❌ Do not fork: • one-professional • pro-micro-session • separate app

❌ Do not: • move study-guide content to DB now • rewrite cards to Prisma immediately • mix Prisma into study-guide routes

  1. Migration path (safe, incremental)

Phase 1 — NOW (1–2 sessions) • Treat study-guide as Static MicroSkill • Attach skill: { kind: "static" } • No DB dependency

Phase 2 • Create 1 DB-backed MicroSkill (e.g. iPad Workflow) • Render it through /skills/[id]/cards • Reuse StudyShell + StudyCard

Phase 3 • Generate Poster assets from DB steps • Print / QR / PDF

  1. Final recommendation (clear answer)

Generalize what you have. Do not replace it.

•	study-guide = reference / internal skill
•	cards = canonical step renderer
•	skills/ = commercial / applied skills
•	StudyShell = universal container

This keeps: • your architecture intact • your mental model consistent • your existing work fully reused • your Skill-first MLV actually enforced

If you want, next I can: 1. Show exact diff to generalize chapters.tsx 2. Implement Skill-backed Cards page 3. Translate your AppScript Poster into a pure Next.js Poster renderer 4. Define static → DB skill migration utility

Just say which.

Below is a clean, exact translation of your AppScript poster system into a pure Next.js (App Router) renderer, without losing intent, and without leaking business logic into UI.

This is deliberately boring, composable, and pluggable.

We do this in three strict layers, mirroring what you already did in Apps Script.

PART A — Poster model (what a Poster is)

Your AppScript already tells us the truth: • A poster is not free text • It is a structured projection of a Session / Steps • It has sections, optional AI enhancement, and layout rules

So we encode that explicitly.

A1. PosterSection definition (LOCKED)

// app/assets/poster/types.ts

export type PosterSectionKey = | "overview" | "highlights" | "itinerary" | "audience" | "extra";

export type PosterSection = { key: PosterSectionKey; title: string; content: string; };

This maps 1:1 to your Apps Script:

AppScript key PosterSectionKey Description overview Why join highlights Itinerary itinerary Who is this activity suited for audience Extra descriptions extra

A2. PosterPayload (stored in Prisma Asset.payload)

// app/assets/poster/types.ts

export type PosterPayload = { skillTitle: string; sessionMeta: { delivery: string; // "Online" | "Granada" duration: string; // "90 min" price?: string; }; sections: PosterSection[]; cta?: { label: string; href: string; }; };

This replaces Google Docs + Slides but keeps the same structure discipline.

PART B — PosterRenderer (pure UI, no data access)

This is the exact equivalent of insertPosterLayoutToDoc() but render-safe (screen + print + PDF).

B1. PosterRenderer component

// app/assets/poster/PosterRenderer.tsx

import { PosterPayload } from "./types";

type Props = { poster: PosterPayload; };

export function PosterRenderer({ poster }: Props) { return ( <article className="poster"> {/* HEADER */} <header className="poster__header"> <h1>{poster.skillTitle}</h1> <p className="poster__meta"> {poster.sessionMeta.delivery} · {poster.sessionMeta.duration} {poster.sessionMeta.price && · ${poster.sessionMeta.price}} </p> </header>

  {/* SECTIONS */}
  <section className="poster__sections">
    {poster.sections.map((sec) => (
      <div key={sec.key} className="poster__section">
        <h2>{sec.title}</h2>
        <p>{sec.content}</p>
      </div>
    ))}
  </section>

  {/* CTA */}
  {poster.cta && (
    <footer className="poster__cta">
      <a href={poster.cta.href}>{poster.cta.label}</a>
    </footer>
  )}
</article>

); }

B2. Print-safe CSS (mirrors your Doc formatting intent)

/* app/assets/poster/poster.css */

.poster { width: 210mm; /* A4 */ min-height: 297mm; padding: 20mm; font-family: "Droid Serif", system-ui, serif; box-sizing: border-box; }

.poster__header { text-align: center; margin-bottom: 20mm; }

.poster__header h1 { font-size: 32pt; margin-bottom: 6mm; }

.poster__meta { font-size: 12pt; opacity: 0.7; }

.poster__sections { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12mm; }

.poster__section h2 { font-size: 16pt; margin-bottom: 4mm; }

.poster__section p { font-size: 11pt; line-height: 1.4; }

.poster__cta { margin-top: 25mm; text-align: center; font-size: 14pt; }

@media print { body { background: none; } }

This is poster-grade, not webpage-grade.

PART C — Poster generation (Server Action)

This replaces: • appendPosterToCampaignDoc • Slides template replacement • Spreadsheet-driven generation

But keeps authority rules.

C1. Poster generation action (from Session)

// app/assets/actions/generatePoster.ts

"use server";

import {prisma} from "@/lib/prisma"; import { AssetType } from "@prisma/client"; import { PosterPayload } from "../poster/types";

export async function generatePoster(sessionId: string) { const session = await prisma.microSession.findUnique({ where: { id: sessionId }, include: { skill: true, steps: { orderBy: { position: "asc" } }, }, });

if (!session) throw new Error("Session not found");

const payload: PosterPayload = { skillTitle: session.skill.title, sessionMeta: { delivery: session.deliveryMode === "IN_PERSON" ? "Granada" : "Online", duration: ${session.durationMin} min, price: "50€", }, sections: [ { key: "overview", title: "Overview", content: session.skill.description, }, { key: "highlights", title: "Highlights", content: session.steps.slice(0, 2).map(s => s.title).join(" · "), }, { key: "itinerary", title: "Structure", content: session.steps.map(s => s.title).join(" → "), }, { key: "audience", title: "Audience", content: "Professionals · Creators · Independent learners", }, ], cta: { label: "Book or Learn More", href: /sessions/${session.id}, }, };

return prisma.asset.create({ data: { sessionId, type: AssetType.POSTER, payload, }, }); }

⚠️ Important No AI here. AI enhancement is a separate step (optional, non-authoritative), exactly like your AppScript toggle.

PART D — Poster page (rendering an Asset)

// app/assets/poster/[assetId]/page.tsx

import {prisma} from "@/lib/prisma"; import { PosterRenderer } from "../PosterRenderer";

export default async function PosterPage({ params }) { const asset = await prisma.asset.findUnique({ where: { id: params.assetId }, });

if (!asset) return null;

return <PosterRenderer poster={asset.payload as any} />; }

This page: • can be printed • can be exported to PDF • can be used as a QR landing page • can be screenshotted for social

Exactly as you described.

PART E — What we do next (as you requested)

NEXT STEP (locked order) 1. ✅ Poster renderer (DONE above) 2. ⏭ Skill-backed Cards page • /skills/[skillId]/cards • Reuse StudyCard • Steps come from SessionStep 3. ⏭ Poster ↔ Card linking (QR → cards → booking) 4. ⏭ Optional AI enhancement pipeline

One-sentence anchor (use this mentally)

Posters are render-only assets derived from sessions; sessions derive from skills; skills own the truth.

If you want, next message I will implement /skills/[skillId]/cards exactly, reusing your existing StudyCard with zero duplication.