$schema: https://raw.githubusercontent.com/AgentsORG/DESIGN/main/schema/design.v1.schema.json
schema: design.v1
name: campus-binge
version: 1.2.1
status: lock
updated_at: "2026-10-29T00:00:00+05:30"
description: >
  Campus Binge, the brand behind Bingy: a calm, green, type-led guide that shows Indian school
  students which colleges they can get into and what to do this week to get there. Everything
  the campusbinge.com website is built from: tokens, every control, surface, overlay and motion.

agent:
  skill: design
  instructions: |
    You are holding Campus Binge's .design file (schema design.v1): the brand's living visual
    contract, written from the live website. If the AgentsORG `design` skill or the
    `design-engineering` skill is installed, use it, and still treat THIS file as the truth.

    READ
    - Load this file before making or reviewing anything that carries the Campus Binge brand:
      pages, app screens, components, decks, social posts, emails, documents, UI copy.
    - Order: overview and intent -> constraints -> decisions -> tokens -> voice -> logo rules
      (assets.logo) -> components -> patterns -> motion (tokens.motion, patterns.motion-*) ->
      rationale.
    - Tokens and structured rules are normative. Notes and rationale are judgment for cases
      the tokens don't settle. Every component lists its real values from the website.

    FOLLOW
    - Use the role tokens; never invent a hex, radius, shadow, size or easing when one exists.
    - Build controls exactly as components.* describe (sizes, states, motion, a11y). Prefer an
      existing component over a new one.
    - Poppins sets headings, buttons and labels; Montserrat sets everything people read.
      Sentence case always. One gradient word per headline (patterns.gradient-word).
    - Spanish Green is for fills, edges, rings and large type. Green words use primary-deep.
      UFO Green is decoration only. No body text on a saturated green surface.
    - Light only. Do not author a dark mode.
    - Motion: transform and opacity only; ease-out for things arriving, springs for things
      people touch, linear only for loops and progress; never transition: all. Every
      animation has a reduced-motion version. Keyboard-opened things never animate.
    - Touch: hover styles only under (hover: hover) and (pointer: fine); press feedback on
      :active; 44x44 targets; no tap highlight; controls aren't text-selectable.
    - Logo: the files in assets.logo, as provided. Obey assets.logo.never.
    - Copy follows voice: a well-informed older sibling. Plain, specific, never hype.

    UPDATE
    - Edit this file in place when the brand or the site changes; git is the history. Bump
      version (SemVer). Ask before changing any path in `locked`.

    VERIFY
    - Before shipping: no stray hex outside tokens; contrast meets policy.a11y; one primary
      action per view; targets at least 44x44; inputs at least 16px; focus visible; a
      reduced-motion variant for every animation; nothing overlaps at 320, 390, 768, 1280 and
      1440px wide; the logo isn't stretched, rotated, recoloured, shadowed, cropped, retyped or
      placed on a busy background.
    - Report drift between this file and the product per token group (added, removed,
      changed) before fixing it.

    PRECEDENCE
    1) The user's explicit instruction  2) This file  3) the design / design-engineering
    skill  4) generic taste skills  5) model defaults

    NEVER
    - Invent a parallel design system next to this file.
    - Claim a partnership or endorsement the brand hasn't made.

overview: |
  Campus Binge is for a seventeen-year-old choosing a stream, an exam and a college, often on a
  cheap Android phone, often stressed about a rank. Everything about the brand is calm: white
  space and Poppins carry the hierarchy, Spanish Green is the one brand colour, and playfulness
  is rationed to moments a student earns. The isometric stickers and the low-poly penguin carry
  the warmth so the interface doesn't have to. Controls feel physical: they answer the press,
  spring when touched, and never make anyone wait.

intent:
  reference: >
    The calm of Family.co and Abode, the clear expressive brand pages of companies like Aave,
    and the physical press of a good button; never the noise of a coaching-centre ad.
  direction: calm-green-type-led
  signature: >
    One word of each headline in the green gradient, the low-poly penguin, and isometric
    stickers that play once and then breathe.
  treatment: editorial
  density: comfortable
  trust: high

targets:
  - web
  - mobile
  - presentation
  - social

voice:
  register: >
    Peer to peer, like a well-informed older sibling who has been through counselling: plain,
    specific, kind. Hinglish where students would use it ("Sab sorted hai").
  casing: sentence
  terminology:
    cut-off: Use "cut-off", not "closing rank threshold".
    exams: Use the names students use (JEE Main, NEET, CUET, JoSAA, board exams).
    bingy: Bingy is the guide; Campus Binge is the company and the app.
    plan: Say "your map" or "your road" for a student's plan.
    soon: Anything not live yet is labelled "Soon" in a dark pill; never implied to work.
  action_naming: >
    Buttons say what happens, in the student's words: "Start my map", "Save my map",
    "See how it works", "Bring Bingy to your school", "Copy as SVG".
  errors: >
    Say what happened and what to do next. Never blame the student, never show a stack trace,
    never open with "Oops".
  never:
    - 'The phrase "Unlock your potential"'
    - 'The phrase "AI-powered" as a selling line'
    - Emoji-stuffed headlines
    - Jargon a student or parent wouldn't say ("harness", "MCP", "B2B", "career intelligence engine")

tokens:
  color:
    # Brand (the live site's :root; shown on the brand page)
    primary: { $value: "#019543", description: "Spanish Green. Fills, buttons, rings, large display type, slider fill." }
    primary-deep: { $value: "#017835", description: "Green that carries words; pressed buttons; gradient start." }
    on-primary: "#FFFFFF"
    accent: { $value: "#46E67C", description: "UFO Green. Decoration only, never text." }
    on-accent: "#01742D"
    primary-container: { $value: "#C9F7D8", description: "Mint. Chips, check marks, quiet brand moments." }
    on-primary-container: "#01742D"
    # Surfaces and ink
    surface: "#FFFFFF"
    surface-tint: { $value: "#ECFDF2", description: "Green wash: active nav item, fixed state, tint squares." }
    surface-alt: { $value: "#F7F7F7", description: "Cloud: secondary buttons, inputs, wells, menu hover." }
    heading: { $value: "#3C3C3C", description: "Headings, strong labels, the selected pill of a segmented control." }
    text: { $value: "#4B4B4B", description: "Body text. Warm, never near-black." }
    text-muted: { $value: "#6B6B6B", description: "Captions, standfirsts, unselected options." }
    text-faint: "#AFAFAF"
    outline: { $value: "#E5E5E5", description: "Card edges, hairlines, input borders, the slider track." }
    outline-soft: "#EFEFEF"
    dot: "#DCDCDC"
    selection: { $value: "#E3F3FE", description: "Text selection background." }
    danger: { $value: "#E5484D", description: "Destructive, closed roads and the 'don't' slash only, always with a word." }
    # Brand-guide neutrals
    dark-liver: { $value: "#4D4D4D", description: "The wordmark's BINGE." }
    dark-silver: "#707070"
    light-gray: "#D3D3D3"
    # Support hues: each has a fill, a deep (text/edge) and a wash (background)
    sky: "#1899E8"
    sky-deep: "#1279BF"
    sky-wash: "#E3F3FE"
    mango: "#FF9A1F"
    mango-ink: "#A95600"
    mango-wash: "#FFF0DC"
    rose: "#FF5A5F"
    rose-ink: "#C53238"
    rose-wash: "#FFE8E9"
    berry: "#8C6CFF"
    berry-deep: "#6A4FD6"
    berry-wash: "#F1EDFF"
    sun: "#FFC21A"
    sun-ink: "#5C4300"
    sun-wash: "#FFF6D6"
    # Road kinds: every exam, college, skill... on a career map has a kind colour
    kind-exam: "#7C5CFC"
    kind-exam-deep: "#5B41C9"
    kind-study: "#1E88E5"
    kind-study-deep: "#1565C0"
    kind-admission: "#F2862E"
    kind-admission-deep: "#B35A0C"
    kind-skill: "#14B8A6"
    kind-skill-deep: "#0B7A6E"
    kind-work: "#019543"
    kind-work-deep: "#017835"
    kind-money: "#EAB308"
    kind-money-deep: "#8A6A00"
    kind-life: "#EC4899"
    kind-life-deep: "#C2337F"

  gradient:
    text: { $value: "linear-gradient(110deg in oklab, #017835, #019543)", description: "The one gradient word in a headline (background-clip: text)." }
    brand: { $value: "linear-gradient(135deg, #017835 0%, #46E67C 100%)", description: "Large brand surfaces." }
    panel: { $value: "radial-gradient(120% 140% at 100% 0%, #0BBF5A 0%, #019543 45%, #017835 100%)", description: "Big green panels (the brand kit card)." }
    usage: Brand greens only. Never behind body copy. One gradient word per headline.

  typography:
    display: { fontFamily: Poppins, fallbacks: [system-ui, sans-serif], fontSize: "clamp(42px, 6.6vw, 84px)", fontWeight: 800, lineHeight: 1.02, letterSpacing: "-0.035em" }
    h1: { fontFamily: Poppins, fontSize: "clamp(40px, 7vw, 76px)", fontWeight: 700, lineHeight: 1, letterSpacing: "-0.04em" }
    h2: { fontFamily: Poppins, fontSize: "clamp(30px, 4.4vw, 52px)", fontWeight: 800, lineHeight: 1.08, letterSpacing: "-0.028em" }
    h3: { fontFamily: Poppins, fontSize: 20, fontWeight: 600, lineHeight: 1.2 }
    lede: { fontFamily: Montserrat, fontSize: "clamp(16px, 1.5vw, 19px)", fontWeight: 500, lineHeight: 1.55 }
    body-lg: { fontFamily: Montserrat, fallbacks: [system-ui, sans-serif], fontSize: 18, fontWeight: 500, lineHeight: 1.55 }
    body: { fontFamily: Montserrat, fontSize: 16, fontWeight: 500, lineHeight: 1.5 }
    body-sm: { fontFamily: Montserrat, fontSize: 14, fontWeight: 500, lineHeight: 1.45 }
    caption: { fontFamily: Montserrat, fontSize: 12.5, fontWeight: 500 }
    eyebrow: { fontFamily: Poppins, fontSize: 14, fontWeight: 600, lineHeight: 1.3, color: "{tokens.color.primary-deep}" }
    label-caps: { fontFamily: Poppins, fontSize: 12, fontWeight: 700, letterSpacing: "0.12em", textTransform: uppercase }
    button: { fontFamily: Poppins, fontSize: 15, fontWeight: 700 }
    control: { fontFamily: Poppins, fontSize: 13.5, fontWeight: 600 }
    numeric: { fontFamily: Montserrat, fontFeature: "tabular-nums" }

  radius:
    chip: 8
    control: 12
    card: 16
    sheet: 22
    big: 28
    pill: 9999
    squircle: "Where corner-shape: squircle is supported, radii scale by 1.5 and become squircles."

  spacing:
    gutter: 20
    section: "clamp(56px, 8vw, 104px)"
    nav-height: 64
    stack-xs: 6
    stack-sm: 10
    stack-md: 16
    stack-lg: 24
    stack-xl: 32

  size:
    target-min: 44
    button: 48
    button-sm: 40
    button-lg: 56
    input: 50
    segmented: 36
    tile: 44
    tile-sm: 40
    slider-track: 10
    slider-thumb: 30
    switch-width: 44
    switch-height: 26
    menu-row: 36
    menu-row-touch: 44

  elevation:
    raised: "0 0 0 1px rgb(15 23 19 / .05), 0 1px 2px rgb(15 23 19 / .06), 0 6px 16px -8px rgb(15 23 19 / .18)"
    press: "0 0 0 1px rgb(15 23 19 / .06), 0 1px 1px rgb(15 23 19 / .06)"
    float: "0 0 0 1px rgb(15 23 19 / .06), 0 1px 2px rgb(15 23 19 / .06), 0 4px 8px -2px rgb(15 23 19 / .06), 0 12px 24px -6px rgb(15 23 19 / .1), 0 32px 64px -16px rgb(15 23 19 / .16)"
    primary-glow: "0 8px 20px -10px rgb(1 149 67 / .7)"
    sticker: "drop-shadow(0 12px 16px rgb(20 60 35 / .14))"

  motion:
    ease-out: { $value: "cubic-bezier(.23, 1, .32, 1)", description: "Anything arriving or answering: menus, tooltips, presses, reveals." }
    ease-in-out: { $value: "cubic-bezier(.77, 0, .175, 1)", description: "Things moving across the screen; idle loops." }
    ease-drawer: { $value: "cubic-bezier(.32, .72, 0, 1)", description: "Sheets and the iOS-style notification stack." }
    ease-spring: { $value: "linear(0, .028, .1, .199, .314, .433, .549, .657, .753, .836, .904, .959, 1, 1.03, 1.05, 1.062, 1.067, 1.068, 1.064, 1.058, 1.051, 1.043, 1.035, 1.027, 1.02, 1.014, 1.009, 1.005, 1.001, .999, .997, .996, 1)", description: "About 7% overshoot: button release, toggles, reveals that rise." }
    ease-bouncy: { $value: "linear(0, .009, .035 2.1%, .141, .281 6.7%, .723 12.9%, .938 16.7%, 1.017, 1.077, 1.121, 1.149 24.3%, 1.159, 1.163, 1.161, 1.154 29.9%, 1.129 32.8%, 1.051 39.6%, 1.017 43.1%, .991, .977 51%, .974 53.8%, .975 57.1%, .997 69.8%, 1.003 76.9%, 1)", description: "Marketing pops only: stickers, the ink pill, a Soon badge." }
    press: 100
    micro: 160
    default: 240
    menu-in: 260
    menu-out: 140
    sheet: 500
    reveal: 450
    reveal-rise: 700
    stagger: 80
    word-stagger: 45
    press-scale: 0.95
    loop-cruise: "30-38px per second"
    reduced: >
      Under prefers-reduced-motion: no movement or scaling; keep short opacity fades (100-200ms)
      where they help comprehension; loops stop; sticker idles stop; reveals show instantly.

themes:
  default: light
  single: true
  reason: Light only is a brand decision, not an omission.

components:
  button:
    import: "styles.css .btn, .btn-primary, .btn-secondary, .btn-sm, .btn-lg, .btn-block; brand.css .kit-btn (depth)"
    when: ["any action"]
    when_not: ["navigation that is really a link (use a link styled as a button only when it leads somewhere)"]
    variants: [primary, secondary, small, large, block, depth]
    states: [default, hover, active, disabled, focus-visible]
    tokens:
      primary:
        backgroundColor: "{tokens.color.primary}"
        textColor: "{tokens.color.on-primary}"
        typography: "{tokens.typography.button}"
        rounded: "{tokens.radius.pill}"
        height: 48
        padding: "0 22px"
        gap: 8
        boxShadow: "{tokens.elevation.primary-glow}"
      secondary:
        backgroundColor: "{tokens.color.surface-alt}"
        textColor: "{tokens.color.heading}"
        typography: "{tokens.typography.button}"
        rounded: "{tokens.radius.pill}"
        height: 48
        padding: "0 22px"
        boxShadow: "inset 0 0 0 1px {tokens.color.outline}"
      small: { height: 40, padding: "0 16px", fontSize: 14 }
      large: { height: 56, padding: "0 28px", fontSize: 16 }
      depth:
        backgroundColor: "{tokens.color.surface}"
        textColor: "{tokens.color.primary-deep}"
        rounded: "{tokens.radius.pill}"
        height: 52
        boxShadow: "0 4px 0 rgb(0 0 0 / .18)"
    notes: >
      Press: scale to 0.95 in 100ms with ease-out; release springs back over 450ms with
      ease-spring. Primary hover (mouse only): background mixes 10% black; active mixes 16%.
      A trailing arrow icon (18px) nudges 3px right on hover. The depth variant is for one
      commitment action on a coloured panel: pressing moves it down 4px while the shadow
      collapses to 0 (box-shadow, never border, so nothing reflows). Labels never wrap.
      touch-action: manipulation; not selectable.

  segmented-control:
    import: "styles.css .seg-ctl, .ink, .has-ink; interactions.js liquid()"
    when: ["choosing one of 2-5 options in place: weights, languages, sticker style, filters"]
    when_not: ["navigation between pages", "more than 5 options (use chips)"]
    tokens:
      track:
        backgroundColor: "{tokens.color.surface}"
        rounded: "{tokens.radius.pill}"
        padding: 3
        gap: 2
        boxShadow: "inset 0 0 0 1px {tokens.color.outline}"
      option:
        height: 36
        padding: "0 14px"
        typography: "{tokens.typography.control}"
        textColor: "{tokens.color.text-muted}"
        rounded: "{tokens.radius.pill}"
      selected:
        backgroundColor: "{tokens.color.heading}"
        textColor: "{tokens.color.on-primary}"
    notes: >
      The "ink pill": one dark pill slides to the chosen option (transform 520ms ease-bouncy;
      width and height 360ms ease-out) while the chosen label turns white (220ms). Options
      press to 0.96. role=radiogroup with role=radio (or tablist/tab); one tab stop; arrow keys
      move and select. When labels can't fit (under ~520px of card), every option keeps its
      icon and only the chosen one shows its label; the pill grows with it.

  chips:
    import: "styles.css .goal-chips, .kicker, .pill-note"
    when: ["picking from a short list in a hero or form (e.g. 'I want to be'), tags, trust labels"]
    variants: [choice, tint, kicker]
    tokens:
      choice:
        backgroundColor: "{tokens.color.surface}"
        textColor: "{tokens.color.heading}"
        height: 42
        padding: "0 16px"
        rounded: "{tokens.radius.pill}"
        boxShadow: "{tokens.elevation.raised}"
        typography: "{ fontFamily: Poppins, fontSize: 15, fontWeight: 600 }"
      choice-selected:
        backgroundColor: "{tokens.color.heading}"
        textColor: "{tokens.color.on-primary}"
      tint:
        backgroundColor: "{tokens.color.primary-container}"
        textColor: "{tokens.color.on-primary-container}"
        rounded: "{tokens.radius.pill}"
      kicker:
        height: 28
        padding: "0 11px 0 8px"
        rounded: "{tokens.radius.pill}"
        backgroundColor: "the section's kind colour mixed 13% into white (oklch)"
        textColor: "the kind's deep colour"
        typography: "{ fontFamily: Poppins, fontSize: 12.5, fontWeight: 600 }"
    notes: "Choice chips share the ink pill with segmented controls. Kickers sit above a section heading with a 15px icon."

  slider:
    import: "styles.css input[type='range']; brand.css .ty-slide"
    when: ["a continuous or stepped value: weights, marks, ranks, budgets"]
    tokens:
      track:
        height: 10
        rounded: "{tokens.radius.pill}"
        fill: "{tokens.color.primary}"
        rest: "#DDDDDD"
        recipe: "linear-gradient(90deg, primary var(--p), #ddd var(--p)); JS sets --p to the value's percent on every input"
      thumb:
        size: 30
        backgroundColor: "{tokens.color.surface}"
        rounded: "{tokens.radius.pill}"
        boxShadow: "0 0 0 1px rgb(0 0 0 / .08), 0 4px 12px rgb(0 0 0 / .2)"
      thumb-active:
        scale: 1.2
        boxShadow: "0 0 0 8px rgb(1 149 67 / .15), 0 4px 12px rgb(0 0 0 / .2)"
      focus:
        boxShadow: "0 0 0 3px #fff, 0 0 0 5px {tokens.color.primary} on the thumb (never a box around the track)"
    notes: >
      Height 36px hit area; touch-action: pan-y so the page still scrolls. The thumb grows on
      press with ease-bouncy (400ms). The value is shown beside it in tabular numbers. The
      value changes only while the slider is being dragged or keyed; nothing else (pointer
      position, scroll) drives it. Stepped values (e.g. weights 100-900) snap.

  switch:
    import: "styles.css .sw"
    when: ["an on/off setting"]
    tokens:
      track: { width: 44, height: 26, rounded: "{tokens.radius.pill}", off: "#D4D4D4", on: "{tokens.color.primary}" }
      knob: { size: 20, inset: 3, backgroundColor: "{tokens.color.surface}", boxShadow: "0 1px 3px rgb(0 0 0 / .25)", travel: 18 }
    notes: "Knob travels with ease-spring (320ms); track colour 220ms ease-out. role=switch, aria-checked. Hit area extends 9px above and below."

  text-field:
    import: "styles.css .field"
    when: ["any input or select"]
    tokens:
      default:
        height: 50
        padding: "0 14px"
        backgroundColor: "{tokens.color.surface-alt}"
        border: "1px solid {tokens.color.outline}"
        rounded: "{tokens.radius.control}"
        typography: "{ fontFamily: Montserrat, fontSize: 16, fontWeight: 600 }"
        textColor: "{tokens.color.heading}"
      label:
        typography: "{ fontFamily: Poppins, fontSize: 13.5, fontWeight: 600 }"
        gap: 6
    notes: >
      Never under 16px (iOS zooms otherwise). Focus: border turns primary and a soft green
      ring appears (160ms). Selects use a CSS chevron in text-muted. Phone numbers format
      as 98765 43210 while typing. Use inputmode/type/enterkeyhint for the right keyboard.

  tabs:
    import: "brand.css .stage-tabs"
    when: ["switching between versions of one thing in place (logo versions)"]
    tokens:
      rail:
        height: 2
        backgroundColor: "{tokens.color.outline}"
      selected:
        fill: "{tokens.color.primary}"
        motion: "scaleX 0 -> 1 from the left, 500ms ease-out"
      label:
        typography: "{ fontFamily: Poppins, fontSize: 16, fontWeight: 600 }"
    notes: "Each tab is a hairline with a title and one line of description beneath; unselected text dims to muted. Arrow keys move between tabs."

  card:
    import: "styles.css .bx, .mini, .tilt; interactions.js (tilt toward the pointer)"
    when: ["a resting content surface on a marketing page"]
    tokens:
      default:
        backgroundColor: "{tokens.color.surface}"
        rounded: "{tokens.radius.big}"
        boxShadow: "{tokens.elevation.raised}"
        padding: "clamp(20px, 2.4vw, 28px)"
    notes: "On a mouse, cards lean toward the pointer (at most ~2.5deg, perspective 900px, 700ms ease-spring back) with a soft light following the cursor. Never on touch."

  tint-square:
    import: "styles.css .tint-sq"
    when: ["showing the product inside a section: a demo sits in a pastel rounded square"]
    tokens:
      default:
        backgroundColor: "the section's kind wash (or surface-tint)"
        rounded: "{tokens.radius.big}"
        padding: "clamp(16px, 3vw, 40px)"
    notes: "The demo inside is a white card with elevation.raised. Demos loop while on screen, pause after a visitor touches them, and are skipped under reduced motion."

  tile:
    import: "styles.css .tile"
    when: ["a kind icon on a career map, a list row, an app row"]
    tokens:
      default:
        size: 44
        rounded: "{tokens.radius.control}"
        backgroundColor: "the kind colour"
        iconColor: "#FFFFFF"
        iconSize: "46%, stroke 2.4"
        boxShadow: "0 1px 2px (kind-deep 55%), 0 6px 14px -6px (kind-deep 60%), inset 0 1px 0 rgb(255 255 255 / .22)"
        gloss: "a small white highlight at the top-left, 45% opacity"

  faq:
    import: "styles.css .qa"
    when: ["questions and answers"]
    tokens:
      item:
        backgroundColor: "{tokens.color.surface}"
        rounded: "{tokens.radius.card}"
        boxShadow: "{tokens.elevation.raised}"
      summary:
        minHeight: 56
        padding: "18px 20px"
        typography: "{ fontFamily: Poppins, fontSize: 16.5, fontWeight: 700 }"
        chevron: "9px, 2.5px stroke in primary-deep, rotates 180deg in 300ms ease-out"
    notes: "Native <details name> so one is open at a time. The answer opens by height (block-size with interpolate-size, 260ms ease-out) and fades up. Summary presses to a 3.5% ink wash."

  nav:
    import: "styles.css .nav, .nav-links"
    when: ["the page header"]
    tokens:
      bar:
        height: "{tokens.spacing.nav-height}"
        backgroundColor: "rgb(255 255 255 / .96)"
        scrolled: "0 1px 0 rgb(15 23 19 / .08), 0 8px 24px -16px rgb(15 23 19 / .2)"
      link:
        padding: "10px 14px"
        rounded: "{tokens.radius.pill}"
        typography: "{ fontFamily: Poppins, fontSize: 14.5, fontWeight: 600 }"
        active: "primary-deep text on surface-tint"
    notes: "Logo left, section links centre (hidden on phones), one small primary button right. Fixed; gains its shadow after 8px of scroll; slides away during full-screen scenes. Pads the top safe area."

  dock:
    import: "styles.css .dock"
    when: ["phones: the main action once the hero's button has scrolled away"]
    notes: "A full-width primary button pinned to the bottom with a white fade behind it and the bottom safe area added. Slides in with ease-spring (420ms), out in 260ms ease-out. Hidden near the final call to action and during full-screen scenes."

  bottom-sheet:
    import: "styles.css .sheet, .sheet-grab, .sheet-x; interactions.js (pull to dismiss)"
    when: ["the waitlist and any form that takes over"]
    tokens:
      default:
        backgroundColor: "{tokens.color.surface}"
        rounded: "{tokens.radius.sheet} {tokens.radius.sheet} 0 0"
        maxHeight: "92dvh"
        boxShadow: "{tokens.elevation.float}"
      grab: { width: 40, height: 5, color: "{tokens.color.outline}", hitArea: "14px above and below, 80px either side" }
      close: { size: 44, backgroundColor: "{tokens.color.surface-alt}" }
    notes: >
      A <dialog> opened with showModal. Rises with translateY(100%) -> 0 over 500ms
      ease-drawer; closes in 220ms. On touch, drag the grab strip down: it follows the
      finger, rubber-bands past 200px (30%), and dismisses past 120px or on a flick faster
      than 0.11px/ms; otherwise springs back (300ms ease-drawer). Escape and the backdrop
      close it. overscroll-behavior: contain. The page doesn't scroll underneath.

  dialog:
    import: "styles.css .qr-full, .qr-panel"
    when: ["a full-size view of one thing (the QR code)"]
    notes: "Centred white card (radius 32) on a backdrop of rgb(8 20 13 / .72) with a 6px blur. Scales in from 0.96 with opacity in 260ms ease-out (modals stay centred, never scale from the trigger); out in 180ms. A 40px round close button; Escape or a click on the backdrop closes it."

  context-menu:
    import: "context-menu.js (styles are inside it)"
    when: ["right-click anywhere, and long-press on a logo"]
    tokens:
      menu:
        width: 288
        padding: 6
        rounded: 16
        backgroundColor: "{tokens.color.surface}"
        boxShadow: "{tokens.elevation.float}"
      row:
        height: 36
        heightTouch: 44
        padding: "0 10px"
        rounded: 10
        typography: "{ fontFamily: Montserrat, fontSize: 14, fontWeight: 600 }"
        hover: "{tokens.color.surface-alt}"
        icon: "18px, stroked, text-muted; primary-deep when active"
        shortcut: "12px Montserrat, #9A9A9A, right-aligned (⌘ on Mac)"
      tools-row:
        height: 40
        layout: "Back, Forward, Reload, Copy link, Share, Print as one row of icon buttons with a dark tip under each (350ms delay)"
      logo-card:
        layout: "On a logo: a Brand card (44px mint square with the penguin, title and line, arrow), a divider, then Copy as PNG and Copy as SVG side by side"
    notes: >
      Replaces the browser menu everywhere except inputs and with Shift held. Opens at the
      pointer, origin at the nearest corner, flips to stay 8px inside the window; pops from
      0.92 in 260ms with a light overshoot; exits to 0.96 in 140ms. Keyboard: Shift+F10 or
      the Menu key opens it with the first item active and no animation; arrows move, Enter
      picks, Escape/Tab close and return focus. Closes on scroll, resize, blur or a press
      outside. What's under the pointer comes first; each action appears once. Copy actions
      turn their icon into a mint tick and say "Copied" for 750ms before closing.

  tooltip:
    import: "brand.css .bsw-tip; context-menu.js .lm-tool::after"
    when: ["naming an icon-only control, or saying what a click will do"]
    notes: "Dark pill (heading colour, white 12-12.5px Poppins 600), 7x11px padding. Appears after a short delay, scales from 0.96 with opacity (120-140ms ease-out). Pointer-following tips are pinned beside the cursor (20px right, 24px down), never lagging behind it. Not shown on touch."

  toast:
    import: "brand.css .b-toast"
    when: ["confirming something that has no button of its own to say so"]
    notes: "Dark pill bottom-centre, above the safe area. Rises 12px with opacity (260ms ease-out), stays 1.6s. role=status."

  marquee:
    import: "styles.css .marquee, .marquee-track, .mq-g; interactions.js (loops)"
    when: ["logos of backers, press and awards; ability chips"]
    notes: >
      An endless row: the track holds two copies and moves at 30-38px a second (linear).
      Edges fade with a 10% mask. Logos are greyscale at 62% opacity, full colour on hover.
      It can be grabbed and flung: drag moves it, release keeps the fling and eases back to
      cruise speed; a drag never counts as a click. Pauses off screen, on hover and when the
      tab is hidden. Group labels ("Backed by", "Supported by", "Featured in", "Awarded by")
      ride along as small uppercase cloud pills.

  sticker:
    import: "styles.css .idle-bob, .idle-rock, .idle-breathe; assets: stickers"
    when: ["warmth and wayfinding: section icons, hero clusters, empty states"]
    when_not: ["behind text", "more than one cluster per view"]
    notes: >
      Isometric blocks: road, goal, exams, admission, study, skills, money, work, campus life,
      home, chat, call, vault, questions. Still images by default; live versions play their
      own entrance once when first seen (capped at three loops), then idle: bob (4.6s, 7px),
      rock (5.4s, 3.5deg) or breathe (4s, 5%) with ease-in-out. They never re-enter or exit.
      A soft drop shadow (elevation.sticker). On a mouse they can be picked up and spring home.

  notification-stack:
    import: "styles.css .story .msg-c"
    when: ["showing many messages arriving at once on a phone"]
    notes: "iPhone-style: full-width cards (radius 22, white 90% with an 18px backdrop blur on the top card). The newest drops in from -28px; older ones tuck behind, 22px lower and 6% smaller each, at 70% and 40% opacity, two visible. 520ms ease-drawer."

  qr-card:
    import: "styles.css .qr-card, .qr-sq; app.js qrSVG()"
    when: ["desktop only, while the hero fills at least half the screen"]
    notes: "Bottom-left, 20-36px from the edges: a white card with a 108px QR (rounded modules, green gradient with a slow glint, the penguin in the centre on a clear plate, error correction Q) and two lines. It encodes the page's own address plus ?join=qr, which opens the waitlist on the phone. Click opens the dialog. Appears after 400ms; hides when the hero leaves."

  store-badges:
    import: "styles.css .stores, .store, .st-body"
    when: ["apps that are coming"]
    notes: "Black badges at 40% opacity, greyscale, with 'Get it on' over 'Google Play' (and App Store, Microsoft). One line always: every size is in em of one font size set by the column's width (container query units). A dark 'Soon' pill sits in the heading, not on each badge."

  app-tiles:
    import: "styles.css .agents, .agent, .agent-call"
    when: ["the agents and messaging apps Bingy will work in"]
    notes: "40px rounded-square app icons (radius 26%), greyscale at 55% until hovered or pressed, name beneath (12px Poppins 600). One line; tiles and gaps scale with the column. Tapping something 'Soon' wiggles it (360ms) and pops the Soon pill to 1.14."

  scrollbar:
    import: "styles.css .page-thumb; interactions.js (page scrollbar)"
    notes: "On a mouse the page draws its own overlay thumb (6px, ink at 32%) that appears while scrolling or within 16px of the edge, fades 800ms later, widens under the pointer and can be dragged. Touch keeps the system bar. Inner panels hide their scrollbars."

  cursor:
    import: "styles.css :root --tl-cursor-*"
    notes: "A custom cursor pack (tldraw's cursors, https://tldraw.dev) for default, pointer, grab, grabbing and text. Anything clickable shows the pointer; draggable rows show grab and grabbing; text shows the text cursor."

patterns:
  gradient-word:
    treatment: editorial
    recipe: >
      Exactly one meaningful word of a headline (not simply the last one) takes
      tokens.gradient.text via background-clip: text, with 0.05em right padding so the last
      letter isn't clipped. The rest stays tokens.color.heading.
  hero:
    treatment: editorial
    recipe: >
      Centred and calm: an early-access pill, a short spoken headline with one gradient word,
      one line of support (lede, max 60ch), one primary and one secondary button, a social
      proof line, then a row of choice chips. Sticker clusters flank the copy, never behind it.
  section:
    recipe: >
      Padding tokens.spacing.section top and bottom; content max 1180px with the gutter. A
      kicker, an h2 with one gradient word, a lede, three short check-marked lines, then the
      demo in a tint square beside it (alternating sides on wide screens, stacked on phones).
  green-surface:
    recipe: >
      A Spanish Green surface never holds raw body text. Use white text only at large sizes,
      put the words on a white card, or use mint (primary-container) instead.
  motion-reveal:
    recipe: >
      Sections rise once as they enter: opacity 0 -> 1 (450ms ease-out) and translate 26px
      -> 0 (700ms ease-spring), staggered 80ms. Headings arrive word by word (45ms apart),
      each rising 0.3-0.45em out of a 6px blur. Once only, never on scroll back.
  motion-feedback:
    recipe: >
      Every pressable thing answers on press, not release: buttons 0.95, options and rows
      0.96-0.98, in 100-160ms ease-out. Copy actions confirm in place (label blurs into
      "Copied" over 200ms, icon becomes a tick). A small "+1 on your map" can rise from a
      button that adds something.
  motion-loops:
    recipe: "Demos and loops run only while on screen, pause when touched and when the tab is hidden, and stop under reduced motion."
  layout-safety:
    recipe: >
      Nothing overlaps or underlaps at any width. Check 320, 390, 768, 1280 and 1440px.
      Rows that must stay on one line scale with container units instead of wrapping.

policy:
  hierarchy: [typography, spacing, contrast, color]
  a11y:
    contrast: "AA: 4.5:1 for body text, 3:1 for large text and UI"
    target: "44x44 minimum"
    motion: "every animation has a reduced-motion variant"
    focus: "3px sky outline, 3px offset, for keyboard focus; sliders ring the thumb"
    note: "Spanish Green on white is 3.89:1, so it carries fills and large type only; green words use primary-deep (5.11:1)."

decisions:
  which-green:
    - if: "green text on white or mint"
      then: { use: "primary-deep or on-primary-container" }
    - if: "a fill, edge, ring, slider fill or icon"
      then: { use: "primary" }
    - if: "decoration or a gradient end"
      then: { use: "accent" }
  which-control:
    - if: "one of 2-5 options, shown in place"
      then: { use: "segmented-control" }
    - if: "a value along a range"
      then: { use: "slider" }
    - if: "on or off"
      then: { use: "switch" }
    - if: "one of many, in a hero or form"
      then: { use: "chips (choice)" }
    - if: "a version of the same thing"
      then: { use: "tabs" }
  which-logo:
    - if: "people may not know Campus Binge yet"
      then: { use: "the full logo" }
    - if: "a square or small space (app icon, avatar, favicon)"
      then: { use: "the head, or the penguin" }
    - if: "on Spanish Green or a photo"
      then: { use: "the white logo" }
    - if: "one-colour print"
      then: { use: "the black logo" }

constraints:
  always:
    - Components reference role tokens, never raw hex
    - Sentence case for headings and buttons
    - Colour is backed by an icon or a word, never the only signal
    - Numbers in columns use tabular-nums
    - Inputs are at least 16px
    - 'Hover effects only under (hover: hover) and (pointer: fine); press feedback on :active'
    - "-webkit-tap-highlight-color: transparent; controls are not text-selectable"
    - Every cut-off, fee and date shown carries its source
    - Anything not live yet is labelled Soon
  never:
    - "transition: all"
    - Animating width, height, top or left (except a measured accordion height)
    - A dark mode
    - Rainbow gradients
    - The colour logo on Spanish Green
    - More than one gradient word per headline
    - Anything following the pointer unless it is being dragged
    - Text over stickers, or stickers behind text

locked:
  - tokens.color.primary
  - tokens.color.primary-deep
  - tokens.color.accent
  - tokens.typography.display.fontFamily
  - tokens.typography.body.fontFamily
  - assets.logo

assets:
  logo:
    full-colour: https://campusbinge.in/landing/assets/brand/logos/logo-color.svg
    full-black: https://campusbinge.in/landing/assets/brand/logos/logo-black.svg
    full-white: https://campusbinge.in/landing/assets/brand/logos/logo-white.svg
    penguin-colour: https://campusbinge.in/landing/assets/brand/logos/mark-color.svg
    penguin-black: https://campusbinge.in/landing/assets/brand/logos/mark-black.svg
    penguin-white: https://campusbinge.in/landing/assets/brand/logos/mark-white.svg
    head-colour: https://campusbinge.in/landing/assets/brand/logos/head-color.svg
    head-black: https://campusbinge.in/landing/assets/brand/logos/head-black.svg
    head-white: https://campusbinge.in/landing/assets/brand/logos/head-white.svg
    versions: "Full logo (penguin + wordmark), wordmark (CAMPUS in Spanish Green over BINGE in Dark Liver), penguin, and head (the square bust: head and beak up top, body cut at the bottom edge)."
    clear-space: "At least the penguin's head on every side."
    never:
      - Stretching or squashing it
      - Rotating or tilting it
      - Changing its colours
      - The colour logo on green
      - Adding shadows, glows or outlines
      - Cropping any part of it
      - Setting the name in another typeface
      - Putting it on a busy background
  favicon: https://campusbinge.in/landing/assets/icons/favicon-32.png
  og_image: https://campusbinge.in/landing/assets/og/og-1200x630.jpg
  stickers:
    road: { still: https://campusbinge.in/landing/assets/blocks/still/block-road.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-road.svg }
    goal: { still: https://campusbinge.in/landing/assets/blocks/still/block-goal.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-goal.svg }
    exam: { still: https://campusbinge.in/landing/assets/blocks/still/block-exam.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-exam.svg }
    admission: { still: https://campusbinge.in/landing/assets/blocks/still/block-admission.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-admission.svg }
    study: { still: https://campusbinge.in/landing/assets/blocks/still/block-study.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-study.svg }
    skill: { still: https://campusbinge.in/landing/assets/blocks/still/block-skill.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-skill.svg }
    money: { still: https://campusbinge.in/landing/assets/blocks/still/block-money.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-money.svg }
    work: { still: https://campusbinge.in/landing/assets/blocks/still/block-work.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-work.svg }
    life: { still: https://campusbinge.in/landing/assets/blocks/still/block-life.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-life.svg }
    home: { still: https://campusbinge.in/landing/assets/blocks/still/block-home.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-home.svg }
    chat: { still: https://campusbinge.in/landing/assets/blocks/still/block-chat.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-chat.svg }
    call: { still: https://campusbinge.in/landing/assets/blocks/still/block-call.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-call.svg }
    vault: { still: https://campusbinge.in/landing/assets/blocks/still/block-vault.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-vault.svg }
    question: { still: https://campusbinge.in/landing/assets/blocks/still/block-question.webp, live: https://campusbinge.in/landing/assets/blocks/live/block-question.svg }
  font_files:
    - "https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap"
    - "https://fonts.googleapis.com/css2?family=Montserrat:wght@100..900&display=swap"
  fonts_note: "Poppins and Montserrat, SIL Open Font License."
  agent_kit: https://campusbinge.in/landing/assets/brand/campus-binge-agent-kit.zip
  brand_page: https://campusbinge.in/brand

# Every file this contract mentions, as a full address. The agent kit zip holds all of them.
exports:
  agent_kit: https://campusbinge.in/landing/assets/brand/campus-binge-agent-kit.zip
  design: https://campusbinge.in/landing/CampusBinge.design
  css:
    path: https://campusbinge.in/landing/styles.css
    selector: ":root"
  css_brand_page: https://campusbinge.in/landing/brand.css
  js_feel: https://campusbinge.in/landing/interactions.js
  js_context_menu: https://campusbinge.in/landing/context-menu.js
  js_brand_page: https://campusbinge.in/landing/brand.js
  js_site: https://campusbinge.in/landing/app.js
  fonts_css: "https://fonts.googleapis.com/css2?family=Montserrat:wght@100..900&family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap"
  note: >
    Component `import` fields name a file above and the selectors or functions inside it. In the
    agent kit the same files sit in code/, the logos in logos/ and the stickers in stickers/.

rationale:
  overview: >
    The brand page at assets.brand_page shows these rules working: the logo's versions, the
    colour bento, both typefaces, the stickers, and the mistakes to avoid.
  dos:
    - Let type and white space carry the hierarchy before reaching for colour
    - Keep the penguin and stickers for warmth, never decoration without a job
    - Spend motion on moments a student earns; answer every press instantly
  donts:
    - Two distinctive typefaces fighting; Poppins leads and Montserrat serves
    - Copy that sounds like a coaching-centre ad
    - Motion that makes people wait or follows them around

provenance:
  owner: Campus Binge
  contact: https://campusbinge.in/contact

sources:
  - type: url
    ref: https://campusbinge.in/brand
    note: The brand page, with the logo versions, palette, type, stickers and rules
  - type: url
    ref: https://campusbinge.in/
    note: Tokens and component values read from styles.css, brand.css, interactions.js and context-menu.js, October 2026
