diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9ea85b2e..cd87a612 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -360,8 +360,38 @@ lands, reviewers should flag any new `style={{ ... }}` prop.
---
+## Code Style
+
+### Frontend Styling
+
+Frontend styling conventions are documented in
+[`frontend/docs/styling.md`](frontend/docs/styling.md). Please read that
+document before making UI changes.
+
+**No inline styles.** Do not use the `style` prop on JSX elements. All
+styling must go through the project's approved mechanisms (e.g. Tailwind
+utility classes, CSS Modules, or the shared design-system components).
+Inline styles bypass theming, break static analysis, and make visual
+regression testing unreliable. If you believe an inline style is
+unavoidable, raise it in the PR description and get maintainer sign-off
+before merging.
+
+---
+
## Security
+Please **do not** report security vulnerabilities through public GitHub issues,
+discussions, or pull requests.
+
+Instead, report them privately via **GitHub Security Advisories** for this
+repository:
+
+- https://github.com/solutions-plug/predictIQ/security/advisories/new
+
+This is the verified, monitored channel for security reports. See
+[`SECURITY.md`](SECURITY.md) for the full disclosure policy and response
+timelines.
+
If you discover a security vulnerability, please **do not** open a public issue.
Instead, report it privately to the maintainers so it can be triaged and fixed
before disclosure.
diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
new file mode 100644
index 00000000..499dc711
--- /dev/null
+++ b/frontend/.eslintrc.js
@@ -0,0 +1,11 @@
+module.exports = {
+ extends: ['next/core-web-vitals'],
+ rules: {
+ 'react/forbid-dom-props': [
+ 'error',
+ {
+ forbid: ['style'],
+ },
+ ],
+ },
+};
\ No newline at end of file
diff --git a/frontend/README.md b/frontend/README.md
index 31ea23ff..2dd13d29 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -2,6 +2,12 @@
This package contains the Handsoff web frontend.
+## Documentation
+
+- [Styling conventions](../docs/styling.md) — CSP-safe styling rules and conventions for frontend contributors.
+- [Internationalization (i18n) guide](./I18N_GUIDE.md) — how to add and manage translations in the frontend.
+- [`docs/`](./docs/) — additional frontend documentation.
+
## Styling conventions (CSP-safe, no inline styles)
### Why: the CSP constraint
@@ -55,4 +61,4 @@ a review blocker.
- [Internationalization (i18n) guide](./I18N_GUIDE.md) — how to add and manage
translations in the frontend.
-- [`docs/`](./docs/) — additional frontend documentation.
\ No newline at end of file
+- [`docs/`](./docs/) — additional frontend documentation.
diff --git a/frontend/docs/styling.md b/frontend/docs/styling.md
index 21f070ed..e71e5e5a 100644
--- a/frontend/docs/styling.md
+++ b/frontend/docs/styling.md
@@ -1,60 +1,134 @@
-# Frontend Styling Conventions
+# Styling Guide
-## The rule: no inline `style` props
+## CSP Constraint: Inline `style` Props Are Not Allowed
-Do **not** use the `style` prop on React components or DOM elements for anything
-that affects rendering:
+This project runs under a strict **Content Security Policy (CSP)** in production. The policy does **not** include `'unsafe-inline'` for styles, which means:
-```jsx
-// ❌ Don't do this
-
…
+- Inline `style` attributes on DOM elements are **stripped or blocked** by the browser.
+- React's `style={{ ... }}` prop compiles down to an inline `style` attribute, so it is affected by the same restriction.
+- The failure is **silent**: no exception is thrown, no console error is guaranteed, and the element simply renders without the intended styles. This makes it easy to ship a broken UI without noticing during development (where CSP is often relaxed).
+
+Because of this, **do not use the `style` prop** on DOM elements or on components that forward it to the DOM. Use CSS classes or CSS modules instead.
+
+## Required Pattern: CSS Classes / CSS Modules
+
+The migration established the following pattern for all styling:
+
+1. **Static styles** live in a stylesheet (global CSS or a CSS module).
+2. **Dynamic styles** are expressed by toggling **class names**, not by computing inline style objects.
+3. Components receive class names via the `className` prop.
+
+### Incorrect
+
+```tsx
+// ❌ Inline style prop — stripped/blocked by CSP in production.
+function ProgressBar({ percent }: { percent: number }) {
+ return (
+
+ );
+}
+```
+
+```tsx
+// ❌ Passing a style object through to a DOM element.
+function Badge({ color, children }: { color: string; children: React.ReactNode }) {
+ return {children};
+}
```
-Instead, express styling through CSS classes (or CSS modules) and apply them via
-`className`:
+### Correct
+
+Use a CSS module (or global class) and toggle classes:
+
+```css
+/* ProgressBar.module.css */
+.progress {
+ height: 4px;
+ background: var(--color-accent);
+}
-```jsx
-// ✅ Do this
-…
+.width0 { width: 0%; }
+.width25 { width: 25%; }
+.width50 { width: 50%; }
+.width75 { width: 75%; }
+.width100 { width: 100%; }
```
-## Why: Content Security Policy strips inline styles
+```tsx
+// ✅ Class-based styling — CSP-safe.
+import styles from "./ProgressBar.module.css";
+
+const WIDTH_CLASS: Record = {
+ 0: styles.width0,
+ 25: styles.width25,
+ 50: styles.width50,
+ 75: styles.width75,
+ 100: styles.width100,
+};
+
+function ProgressBar({ percent }: { percent: number }) {
+ const widthClass = WIDTH_CLASS[percent] ?? styles.width0;
+ return ;
+}
+```
+
+```tsx
+// ✅ Variant classes instead of inline color.
+import styles from "./Badge.module.css";
+
+function Badge({ variant, children }: { variant: "info" | "warn"; children: React.ReactNode }) {
+ return {children};
+}
+```
+
+For values that are genuinely dynamic and cannot be enumerated as classes, prefer:
-Our production Content Security Policy does not allow inline styles. When the
-browser enforces that policy, inline `style` attributes are **silently dropped** —
-the element still renders, but without the styling. This is not a build error and
-not a test failure; it only shows up in production, which is exactly why it went
-unnoticed for so long.
+- A CSS custom property set via a class or a small set of classes, or
+- A dedicated stylesheet rule keyed off a `data-*` attribute, or
+- A CSS-in-JS solution that emits a real stylesheet (not inline attributes).
-This was a real production bug. Two commits fixed it:
+If you believe you have a case that truly requires an inline style, raise it for review before adding one — it will not work in production.
+
+## Enforcement Gap
+
+There is currently **no lint rule** that catches inline `style` props. This is why the CSP violations were able to land in the first place. To prevent regressions, add the following ESLint rules:
+
+```jsonc
+// .eslintrc (excerpt)
+{
+ "rules": {
+ "react/forbid-dom-props": [
+ "error",
+ { "forbid": [{ "propName": "style", "message": "Inline style props are blocked by CSP. Use a CSS class or CSS module instead." }] }
+ ],
+ "react/forbid-component-props": [
+ "error",
+ { "forbid": [{ "propName": "style", "message": "Inline style props are blocked by CSP. Use a CSS class or CSS module instead." }] }
+ ]
+ }
+}
+```
-- `5bd5e51` — moved AppShell chrome off inline styles (CSP was dropping them)
-- `e80a15b` — migrated every remaining inline `style` prop to CSS classes
+`react/forbid-dom-props` covers intrinsic elements (``), while `react/forbid-component-props` covers custom components that may forward `style` to the DOM. Both are needed to close the gap.
-Because the failure mode is silent and environment-specific, the constraint is
-easy to reintroduce by accident. That is why it is documented here: so a future
-PR does not ship the same bug again.
+Until such a rule is enabled, this convention is enforced by review. Please keep it in mind when reviewing frontend changes.
-## The required pattern
+## Historical Context
-- Put styling in CSS (global stylesheets or CSS modules) and reference it with
- `className`.
-- For dynamic values that genuinely must vary at runtime, prefer toggling a
- class (e.g. `className={isActive ? 'is-active' : ''}`) or setting a CSS custom
- property through a class rather than writing an inline `style` object.
-- If you believe you have a case that truly requires an inline style, raise it in
- the PR description first — do not add one silently.
+This constraint was discovered the hard way. Two commits fixed the fallout from inline styles that were silently dropped under CSP:
-## Enforcement
+- **`5bd5e51`** — initial fix replacing inline `style` props with CSS classes (moved AppShell chrome off inline styles).
+- **`e80a15b`** — follow-up cleanup covering remaining components and edge cases (migrated every remaining inline `style` prop to CSS classes).
-There is currently **no** lint rule or CI check that blocks inline `style` props,
-so nothing mechanically prevents a regression. Adding one is recommended:
+These commits are the reference for the pattern described above. When in doubt, look at how those changes were structured.
-- ESLint's [`react/forbid-dom-props`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/forbid-dom-props.md)
- can be configured with `forbid: ['style']` to flag inline `style` props on DOM
- elements.
-- For components that forward a `style` prop, `react/forbid-component-props`
- covers the same case.
+## Summary
-Until such a rule is enabled, this convention is enforced by review. Please keep
-it in mind when reviewing frontend changes.
+- **Never** use the `style` prop on DOM elements or components that forward it to the DOM.
+- Use **CSS classes / CSS modules** and toggle `className` for dynamic styling.
+- The failure mode is **silent** — it will not show up in dev if CSP is relaxed.
+- Add `react/forbid-dom-props` and `react/forbid-component-props` to enforce this automatically.
+- Until enforcement is enabled, this convention is enforced by review.
diff --git a/frontend/package.json b/frontend/package.json
index af613531..616c5caf 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -59,6 +59,7 @@
"eslint": "^9.20.0",
"eslint-config-next": "16.3.3",
"eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-react": "^7.37.4",
"jest": "^30.4.2",
"jest-axe": "^10.0.0",
"jest-environment-jsdom": "^30.5.0",
@@ -72,4 +73,4 @@
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
-}
+}
\ No newline at end of file