+ isClicked=false
+
+ Orange background, regular text
+
+ alert("Not clicked state")}
+ />
+
+
+
+ isClicked=true
+
+ Gray background, bold text
+
+ alert("Clicked state")}
+ />
+
+
+
+ );
+};
+
+export const DesktopView = {
+ args: {
+ isClicked: false,
+ },
+ parameters: {
+ viewport: {
+ defaultViewport: 'desktop1',
+ },
+ },
+};
+
+export const MobileView = {
+ args: {
+ isClicked: false,
+ },
+ parameters: {
+ viewport: {
+ defaultViewport: 'mobile1',
+ },
+ },
+};
+
+export const MobileViewClicked = {
+ args: {
+ isClicked: true,
+ },
+ parameters: {
+ viewport: {
+ defaultViewport: 'mobile1',
+ },
+ },
+};
+
+
diff --git a/docs/RESPONSIVE-COMPONENTS.md b/docs/RESPONSIVE-COMPONENTS.md
new file mode 100644
index 000000000..489712158
--- /dev/null
+++ b/docs/RESPONSIVE-COMPONENTS.md
@@ -0,0 +1,310 @@
+# Reusable Responsive React Components
+
+Guidelines for building React components that work across devices and compose well into larger UIs. Each rule includes a **why** so the intent is clear when edge cases arise.
+
+---
+
+## Units: Never Use `px` in Styles
+
+Use `rem`, `em`, `vw`, or `vh` as appropriate. Never use `px` for sizing, spacing, or typography.
+
+| Unit | Use for |
+|------|---------|
+| `rem` | Most sizing and spacing — relative to the root font size |
+| `em` | Sizing relative to the current element's font size (e.g., icon inside a button) |
+| `vw` / `vh` | Viewport-relative sizing (e.g., full-screen overlays, hero sections) |
+
+**Why:** Different devices, browsers, and user accessibility settings use different base font sizes. A gap of `16px` is a fixed physical size that ignores this — `1rem` scales with the user's chosen font size, so the layout stays proportional and readable everywhere. This is especially important for users who increase their browser font size for accessibility reasons.
+
+```scss
+// ❌ Wrong
+.card { padding: 16px; font-size: 14px; }
+
+// ✅ Correct
+.card { padding: 1rem; font-size: 0.875rem; }
+```
+
+---
+
+## Width: Fill the Parent, Don't Declare Your Own
+
+Components should not set their own width. Use `width: 100%` (or no width declaration) so the component fills whatever space the parent gives it.
+
+**Why:** A component that hardcodes `width: 400px` can only be used in contexts where 400px makes sense. A component that fills its parent can be placed in a sidebar, a modal, a full-width page column, or a card — and look correct in all of them. Top-level layout containers (pages, sections) are the right place to constrain widths.
+
+```scss
+// ❌ Wrong
+.var-form { width: 600px; }
+
+// ✅ Correct
+.var-form { width: 100%; } // or just omit width entirely
+```
+
+---
+
+## Height: Take It from the Content
+
+Components should not set their height. Let content determine height naturally.
+
+**Exception:** Input fields and buttons should have a defined height, informed by design (Figma). These are interactive controls where a consistent tap/click target size matters across the UI.
+
+**Why:** Hardcoded heights either clip content or leave awkward empty space when content changes (different text lengths, translations, dynamic data). Letting height grow with content makes components resilient.
+
+```scss
+// ❌ Wrong
+.notification-banner { height: 60px; }
+
+// ✅ Correct — let content set height
+.notification-banner { padding: 0.75rem 1rem; }
+
+// ✅ OK for interactive controls (from design spec)
+.var-submit-button { height: 2.75rem; }
+```
+
+---
+
+## Props: Accept All, Spread the Rest
+
+Components should destructure the props they use and spread all remaining props onto the outermost rendered element.
+
+**Why:** A parent passing `data-testid`, `aria-label`, `id`, or any other standard HTML attribute shouldn't be blocked by the component only forwarding named props. Spreading unknown props makes components transparent wrappers that work naturally with testing libraries, accessibility tools, and parent orchestration.
+
+```jsx
+// ❌ Wrong — silently swallows props the parent intended to pass
+function VARSubmitButton({ isSubmitted, onClick }) {
+ return ;
+}
+
+// ✅ Correct
+function VARSubmitButton({ isSubmitted, onClick, ...rest }) {
+ return ;
+}
+```
+
+---
+
+## Shared Props: Merge, Don't Overwrite
+
+When a prop like `className` or `style` is used by the component itself *and* may be passed in by a parent, combine both values — don't choose one or the other.
+
+Use `cx()` (classnames library) to merge class names.
+
+**Why:** If a component applies its own `className` and a parent also passes `className`, silently dropping one breaks either the component's styles or the parent's customization. Merging both lets parents add context-specific overrides (spacing, visibility) without fighting the component's base styles.
+
+```jsx
+import cx from 'classnames';
+
+// ❌ Wrong — parent's className is ignored
+function VARCard({ className, ...rest }) {
+ return ;
+}
+
+// ✅ Correct — both classes are applied
+function VARCard({ className, ...rest }) {
+ return ;
+}
+```
+
+---
+
+## Outer Spacing: None — Let the Parent Decide
+
+Components should have no outer margin or padding. The parent is responsible for applying spacing between its children.
+
+**Why:** A component that adds its own outer margin becomes hard to place. The first instance in a list needs no top margin; the last needs no bottom margin; the margin might conflict with a parent's padding. When the component owns zero outer spacing, any parent can position it exactly as needed using margin, gap, or padding on the parent — without fighting the component's defaults.
+
+```scss
+// ❌ Wrong
+.var-form-divider { margin: 1.5rem 0; }
+
+// ✅ Correct — component has no outer margin; parent applies spacing
+.var-form-divider { border: none; border-top: 0.0625rem solid $color-border-weakest; }
+
+// In the parent form:
+.var-form-section + .var-form-divider { margin-top: 1.5rem; }
+// — or — use gap on a flex/grid parent
+```
+
+---
+
+## Breakpoints: One — Mobile vs Desktop
+
+Use a single device-width breakpoint to distinguish mobile from desktop. Do not add intermediate breakpoints unless there is a specific, design-approved reason.
+
+**Breakpoint:** `48rem` (equivalent to 768px at default font size, tablet/desktop boundary)
+
+```scss
+// $breakpoint-mobile is defined in _vars.scss as 48rem
+
+.my-component {
+ // Desktop styles (default)
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+
+ @media (max-width: $breakpoint-mobile) {
+ // Mobile: single column
+ grid-template-columns: 1fr;
+ }
+}
+```
+
+**Why:** Multiple breakpoints create exponential complexity — each component must be tested and maintained at every breakpoint. A single mobile/desktop split covers the vast majority of use cases, keeps styles predictable, and makes responsive behavior easy to reason about. "Mobile" means single-column; "desktop" means multi-column or side-by-side.
+
+---
+
+## Lists and Grids: Use Flexbox or Grid with the Breakpoint
+
+Components that render lists of items should use CSS flexbox or grid for layout, and use the single breakpoint to adapt column count for mobile.
+
+**Why:** Flexbox and grid handle item sizing and wrapping automatically, eliminating manual column math and making layouts naturally fluid.
+
+Prefer `auto-fill` with `minmax()` over hardcoding a column count. This lets the grid fit as many columns as the available width allows — a laptop gets 3, a 4K monitor gets 6 or more, a phone gets 1 — all without extra breakpoints.
+
+```scss
+// ❌ Wrong — hardcodes 3 columns regardless of screen width
+.project-list {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+}
+
+// ✅ Correct — fills columns based on available space
+.project-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
+ gap: 1rem;
+}
+```
+
+`minmax(18rem, 1fr)` means: each column is at least `18rem` wide, and columns share leftover space equally. **The minimum width should reflect the content** — what is the narrowest the item can be before it becomes hard to read or use? This varies significantly by content type:
+
+| Content type | Suggested minimum | Why |
+|---|---|---|
+| Small tags, chips, avatars | `6rem`–`8rem` | Items are compact; many fit comfortably side by side |
+| Project cards with title + description | `16rem`–`20rem` | Need enough width to read a headline and a line or two of text |
+| Article highlights with image + summary | `22rem`–`28rem` | Richer content needs more horizontal space to avoid feeling cramped |
+| Data table rows | avoid `auto-fill` | Use explicit columns; table cells have fixed semantic relationships |
+
+When the minimum is set appropriately, the grid collapses to one column automatically on narrow screens — no explicit `$breakpoint-mobile` is needed just to control column count.
+
+**Use `$breakpoint-mobile` when the item's internal layout needs to change**, not just how many columns there are. A common case is article highlights, where on mobile you might want the image stacked above the text rather than side by side:
+
+```scss
+// Article highlight grid — columns auto-fit, image/text layout changes at mobile
+.article-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr));
+ gap: 1.5rem;
+}
+
+.article-card {
+ display: flex;
+ flex-direction: row; // image left, text right on desktop
+ gap: 1rem;
+
+ @media (max-width: $breakpoint-mobile) {
+ flex-direction: column; // image stacked above text on mobile
+ }
+}
+```
+
+// Horizontal row that wraps — each item fills evenly
+.tag-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+Prefer `gap` over margins between items — `gap` only applies between items, not on the outside edges.
+
+---
+
+## Storybook Tests
+
+Every component needs Storybook stories. Stories serve as living documentation, catch visual regressions before they reach production, and make regression testing much faster as the project evolves.
+
+**Why:** A component with no stories is untestable in isolation. Stories also let designers and other developers see and interact with a component without needing to wire it into a page — which makes review and iteration much faster. As the codebase grows, stories provide a quick way to verify that existing components still render correctly after changes elsewhere in the project.
+
+### Cover Every Meaningful Prop Combination
+
+Write a story for each distinct visual state or behavior the component supports. If a prop changes what the component renders, there should be a story for it.
+
+```javascript
+// ✅ Each prop state gets its own story
+export const Default = { args: { isSubmitted: false } };
+export const Submitted = { args: { isSubmitted: true } };
+export const WithError = { args: { showError: true } };
+export const WithoutError = { args: { showError: false } };
+```
+
+### Viewport Stories for Responsive Components
+
+Any component that uses `$breakpoint-mobile` in its SCSS **must** have both a `DesktopView` and a `MobileView` story using Storybook viewport parameters. Do not use wrapper divs with `maxWidth` to simulate this.
+
+**Why:** Viewport parameters actually resize the Storybook canvas, triggering real CSS media queries. A `maxWidth` wrapper div only constrains the element's box — it does not trigger `@media (max-width: ...)` rules, so the mobile layout never actually renders.
+
+```javascript
+export const DesktopView = {
+ args: { /* ... */ },
+ parameters: {
+ viewport: { defaultViewport: 'desktop1' }, // 1024px canvas
+ },
+};
+
+export const MobileView = {
+ args: { /* ... */ },
+ parameters: {
+ viewport: { defaultViewport: 'mobile1' }, // 375px canvas — triggers $breakpoint-mobile
+ },
+};
+```
+
+Available viewports (defined in `.storybook/preview.js`):
+- `mobile1` — 375px
+- `tablet1` — 768px (at the breakpoint boundary)
+- `desktop1` — 1024px
+
+### Interaction Tests for Interactive Components
+
+Buttons, inputs, toggles, and modals should use the `play` function to verify behavior, not just appearance.
+
+```javascript
+import { within, userEvent } from '@storybook/testing-library';
+import { expect } from '@storybook/jest';
+
+export const Clicked = {
+ args: { isSubmitted: false },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const button = canvas.getByRole('button');
+ await userEvent.click(button);
+ expect(button).toBeDisabled();
+ },
+};
+```
+
+### Story Organization
+
+Follow this order within a story file:
+
+1. `Default` — component with baseline props, no wrappers
+2. `[StateName]` — one story per meaningful prop variation (e.g., `Submitted`, `WithError`)
+3. `DesktopView` / `MobileView` — required for any component with responsive styles
+4. `InContext` — component inside a realistic parent form or layout (optional but helpful for complex components)
+
+---
+
+## Summary Checklist
+
+Before committing a new component, verify:
+
+- [ ] No `px` units in SCSS — using `rem`, `em`, `vw`, or `vh`
+- [ ] No hardcoded `width` (except layout containers)
+- [ ] No hardcoded `height` (except buttons/inputs per design spec)
+- [ ] Unknown props spread onto the outermost element (`...rest`)
+- [ ] `className` (and `style` if applicable) merged with `cx()`, not overwritten
+- [ ] No outer `margin` or `padding` on the component root
+- [ ] Responsive behavior uses only the `$breakpoint-mobile` variable (`48rem`)
+- [ ] Lists/grids use flexbox or grid with `gap`, not margins between items
+- [ ] Storybook story for each meaningful prop state
+- [ ] `DesktopView` and `MobileView` stories for any component with responsive styles
+- [ ] `play` function interaction tests for buttons, inputs, and interactive controls
diff --git a/docs/VAR-COMPONENTS-SPEC.md b/docs/VAR-COMPONENTS-SPEC.md
new file mode 100644
index 000000000..d6c21f8e9
--- /dev/null
+++ b/docs/VAR-COMPONENTS-SPEC.md
@@ -0,0 +1,936 @@
+# Volunteer Activity Reporting (VAR) Components Specification
+
+## Overview
+This document specifies all components needed for the Volunteer Activity Reporting feature. Each component includes props, file locations, storybook tests, and implementation questions.
+
+**Component Pattern:** All new VAR components are implemented as functional components (not class components). This follows modern React best practices and simplifies the codebase.
+
+**Data Collection Pattern:** All form components use standard HTML input elements with `name` attributes. Form data is collected by `MyActivityController` on submission using native form serialization, following the existing pattern used in `CreateProjectController` and other form controllers in this project. No `onUpdate` callbacks are used.
+
+---
+
+## Phase 1: Simple Components (No Dependencies)
+
+### 1. VARFormDivider ([#1121](https://github.com/DemocracyLab/CivicTechExchange/issues/1121))
+**Purpose:** Visual separator between form sections
+
+**Figma:** [VAR Form Divider](https://www.figma.com/design/WADcmVjJh5ARVoZ09xlpfdFN/DemocracyLab?node-id=39337-89449&t=2mfsTMQ1bhMOILO0-4)
+
+
+
+**Files:**
+- Component: `common/components/componentsBySection/VolunteerActivityReporting/VARFormDivider.jsx`
+- Story: `common/components/componentsBySection/VolunteerActivityReporting/stories/VARFormDivider.stories.jsx`
+- Styles: `civictechprojects/static/css/partials/_VARFormDivider.scss` (auto-loaded)
+
+**Props:**
+```typescript
+// No props - purely presentational
+
+```
+
+**Storybook Tests:**
+- Default state: Renders visible divider
+- Responsive design: Confirm divider resizes correctly
+
+**Questions:**
+- ✅ None - straightforward component
+
+---
+
+### 2. VARErrorNotification ([#1133](https://github.com/DemocracyLab/CivicTechExchange/issues/1133))
+**Purpose:** Display error messages to users when form validation fails
+
+**Figma:** [VAR Error Notification](https://www.figma.com/design/WADcmVjJh5ARVoZ09xlpfdFN/DemocracyLab?node-id=39340-84880&t=KPcLW0nctl77aaFZ-4)
+
+
+
+**Files:**
+- Component: `common/components/componentsBySection/VolunteerActivityReporting/VARErrorNotification.jsx`
+- Story: `common/components/componentsBySection/VolunteerActivityReporting/stories/VARErrorNotification.stories.jsx`
+- Styles: `civictechprojects/static/css/partials/_VARErrorNotification.scss` (auto-loaded)
+
+**Props:**
+```typescript
+
+```
+
+**Storybook Tests:**
+- Default state (`showError={true}`): Renders warning emoticon and hardcoded message
+- Hidden state (`showError={false}`): Renders nothing
+
+**Questions:**
+1. ✅ Should the error message be hardcoded or customizable via props? **Answer: Hardcoded**
+2. ❓ What's the exact error message text? From Figma: "Please fill in all required fields"
+
+**Updated Props:**
+```typescript
+
+```
+
+---
+
+### 3. VARSubmitButton ([#1134](https://github.com/DemocracyLab/CivicTechExchange/issues/1134))
+**Purpose:** Form submission button with visual state feedback
+
+**Figma:** [VAR Submit Button](https://www.figma.com/design/WADcmVjJh5ARVoZ09xlpfdFN/DemocracyLab?node-id=39340-85026&t=KPcLW0nctl77aaFZ-4)
+
+
+
+**Files:**
+- Component: `common/components/componentsBySection/VolunteerActivityReporting/VARSubmitButton.jsx`
+- Story: `common/components/componentsBySection/VolunteerActivityReporting/stories/VARSubmitButton.stories.jsx`
+- Styles: `civictechprojects/static/css/partials/_VARSubmitButton.scss` (auto-loaded)
+
+**Props:**
+```typescript
+
+```
+
+**Storybook Tests:**
+- Default state (`isClicked={false}`): Orange button, regular text
+- Clicked state (`isClicked={true}`): Gray button, bold text
+- onClick interaction: Verify callback fires
+
+**Questions:**
+1. ✅ Should `isClicked` be `isSubmitted` or `disabled` for clarity? **Answer: Use `isSubmitted`**
+2. ✅ Should the button be disabled when `isClicked={true}` to prevent double-submission? **Answer: Yes**
+
+**Updated Props:**
+```typescript
+
+```
+
+---
+
+## Phase 2: Form Input Components
+
+### 4. VolunteerActivityReportingCardIntro ([#1118](https://github.com/DemocracyLab/CivicTechExchange/issues/1118))
+**Purpose:** Project card header with toggle to log activity
+
+**Figma:** [Var Card Intro](https://www.figma.com/design/WADcmVjJh5ARVoZ09xlpfdFN/DemocracyLab?node-id=39337-90134&t=deWirqbNmFVIPX40-4)
+
+
+
+**Files:**
+- Component: `common/components/componentsBySection/VolunteerActivityReporting/VolunteerActivityReportingCardIntro.jsx`
+- Story: `common/components/componentsBySection/VolunteerActivityReporting/stories/VolunteerActivityReportingCardIntro.stories.jsx`
+- Styles: `civictechprojects/static/css/partials/_VolunteerActivityReportingCardIntro.scss` (auto-loaded)
+
+**Props:**
+```typescript
+
+```
+
+**Form Integration:**
+Renders a checkbox input with `name="project_{projectId}_log_activity"` that will be gathered on form submission.
+
+**Storybook Tests:**
+- Default state: Display project name and toggle
+- Checked state: Toggle is checked by default
+- Unchecked state: Toggle is unchecked, shows "No activity to log" message
+- Interaction: Toggle changes state correctly
+
+**Questions:**
+- ✅ None - follows standard form input pattern
+
+---
+
+### 5. VARSelectWeek ([#1122](https://github.com/DemocracyLab/CivicTechExchange/issues/1122))
+**Purpose:** Week selection dropdown for logging activities
+
+**Figma:** [Var Week Select](https://www.figma.com/design/WADcmVjJh5ARVoZ09xlpfdFN/DemocracyLab?node-id=38877-82877&t=deWirqbNmFVIPX40-4)
+
+
+
+**Files:**
+- Component: `common/components/componentsBySection/VolunteerActivityReporting/VARSelectWeek.jsx`
+- Story: `common/components/componentsBySection/VolunteerActivityReporting/stories/VARSelectWeek.stories.jsx`
+- Styles: `civictechprojects/static/css/partials/_VARSelectWeek.scss` (auto-loaded)
+
+**Props:**
+```typescript
+
+```
+
+**Form Integration:**
+Renders a `