diff --git a/make_post_sell/models/shop.py b/make_post_sell/models/shop.py
index 69437a0..e18a601 100644
--- a/make_post_sell/models/shop.py
+++ b/make_post_sell/models/shop.py
@@ -136,6 +136,9 @@ class Shop(RBase, Base):
# Watch mode: sticky video, autoplay, related content sidebar
watch_mode_enabled = Column(Boolean, default=False)
+ # Grayscale mode: render entire site in black and white
+ grayscale_mode = Column(Boolean, default=False)
+
# Email digest subscriptions
subscriptions_enabled = Column(Boolean, default=True)
diff --git a/make_post_sell/scripts/alembic/versions/c3b37cb2c9cc_add_grayscale_mode_to_shop.py b/make_post_sell/scripts/alembic/versions/c3b37cb2c9cc_add_grayscale_mode_to_shop.py
new file mode 100644
index 0000000..3daf015
--- /dev/null
+++ b/make_post_sell/scripts/alembic/versions/c3b37cb2c9cc_add_grayscale_mode_to_shop.py
@@ -0,0 +1,36 @@
+"""add grayscale_mode to shop
+
+Revision ID: c3b37cb2c9cc
+Revises: 3c0996ae20f3
+Create Date: 2026-02-14 20:13:18.055029
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'c3b37cb2c9cc'
+down_revision = '3c0996ae20f3'
+branch_labels = None
+depends_on = None
+
+from make_post_sell.models.meta import UUIDType
+
+
+def _column_exists(table, column):
+ conn = op.get_bind()
+ result = conn.execute(sa.text(f"PRAGMA table_info({table})"))
+ return any(row[1] == column for row in result.fetchall())
+
+
+def upgrade():
+ if not _column_exists("mps_shop", "grayscale_mode"):
+ op.add_column(
+ "mps_shop",
+ sa.Column("grayscale_mode", sa.Boolean(), nullable=False, server_default="0"),
+ )
+
+
+def downgrade():
+ op.drop_column("mps_shop", "grayscale_mode")
diff --git a/make_post_sell/static/css/common.css b/make_post_sell/static/css/common.css
index 2e1eac3..1ff01aa 100644
--- a/make_post_sell/static/css/common.css
+++ b/make_post_sell/static/css/common.css
@@ -3396,3 +3396,9 @@ textarea {
.price-history-current td {
font-weight: bold;
}
+
+/* Grayscale mode */
+html[data-grayscale="on"] {
+ filter: grayscale(100%);
+ -webkit-filter: grayscale(100%);
+}
diff --git a/make_post_sell/templates/base.j2 b/make_post_sell/templates/base.j2
index 69e72c6..dbb4c37 100644
--- a/make_post_sell/templates/base.j2
+++ b/make_post_sell/templates/base.j2
@@ -14,7 +14,7 @@
{% else %}
{% set theme_preference = 'light' if (shop_default_theme == 1) else 'dark' %}
{% endif %}
-
+
diff --git a/make_post_sell/templates/shop_settings.j2 b/make_post_sell/templates/shop_settings.j2
index ea01f5e..42b0963 100644
--- a/make_post_sell/templates/shop_settings.j2
+++ b/make_post_sell/templates/shop_settings.j2
@@ -755,6 +755,21 @@
+
+
+
+
+
+
+
+
+ When enabled, every element on the site — images, video, text, and UI — renders in black and white. Original files are not modified.
+
+
+
+
diff --git a/make_post_sell/tests/test_functional.py b/make_post_sell/tests/test_functional.py
index 07b5924..8cb582b 100644
--- a/make_post_sell/tests/test_functional.py
+++ b/make_post_sell/tests/test_functional.py
@@ -3604,3 +3604,73 @@ class TestAnalytics(AuthenticatedFunctionalTests):
self.assertIn("Price History", body)
self.assertIn("$20.00", body)
self.assertIn("$3.50", body)
+
+ def test_grayscale_mode_default_off(self):
+ """Test that new shops have grayscale mode disabled by default."""
+ shop = self._create_shop_helper(
+ user_creds=self.user1_creds, shop_params=self.shop1_params
+ )
+
+ self.dbsession.refresh(shop)
+ self.assertFalse(shop.grayscale_mode)
+
+ def test_grayscale_mode_toggle(self):
+ """Test enabling and disabling grayscale mode via settings form."""
+ shop = self._create_shop_helper(
+ user_creds=self.user1_creds, shop_params=self.shop1_params
+ )
+
+ # Enable grayscale mode
+ res = self.testapp.post(
+ f"/s/{shop.id}/settings",
+ {
+ "form_section": "ribbon-settings",
+ "grayscale_mode": "1",
+ "submit": "Save Settings",
+ },
+ status=302,
+ )
+ res = res.follow()
+
+ self.dbsession.expire(shop)
+ self.assertTrue(shop.grayscale_mode)
+
+ # Disable grayscale mode
+ res = self.testapp.post(
+ f"/s/{shop.id}/settings",
+ {
+ "form_section": "ribbon-settings",
+ "grayscale_mode": "0",
+ "submit": "Save Settings",
+ },
+ status=302,
+ )
+ res = res.follow()
+
+ self.dbsession.expire(shop)
+ self.assertFalse(shop.grayscale_mode)
+
+ def test_grayscale_mode_html_attribute(self):
+ """Test that data-grayscale attribute appears in HTML when enabled."""
+ shop = self._create_shop_helper(
+ user_creds=self.user1_creds, shop_params=self.shop1_params
+ )
+
+ # Without grayscale, attribute should not be present
+ res = self.testapp.get(f"/s/{shop.id}/{shop.slug}", status=200)
+ self.assertNotIn('data-grayscale="on"', res.text)
+
+ # Enable grayscale mode
+ self.testapp.post(
+ f"/s/{shop.id}/settings",
+ {
+ "form_section": "ribbon-settings",
+ "grayscale_mode": "1",
+ "submit": "Save Settings",
+ },
+ status=302,
+ )
+
+ # With grayscale, attribute should be present
+ res = self.testapp.get(f"/s/{shop.id}/{shop.slug}", status=200)
+ self.assertIn('data-grayscale="on"', res.text)
diff --git a/make_post_sell/views/shop.py b/make_post_sell/views/shop.py
index 7da932c..a37c317 100644
--- a/make_post_sell/views/shop.py
+++ b/make_post_sell/views/shop.py
@@ -624,6 +624,20 @@ def shop_settings(request):
)
)
+ # Handle grayscale mode setting
+ grayscale_mode_value = request.params.get("grayscale_mode")
+ if grayscale_mode_value is not None:
+ grayscale_mode = int(grayscale_mode_value) == 1
+ if shop.grayscale_mode != grayscale_mode:
+ shop.grayscale_mode = grayscale_mode
+ status = "enabled" if grayscale_mode else "disabled"
+ request.session.flash(
+ (
+ f"Grayscale mode {status}",
+ "success",
+ )
+ )
+
# Handle stripe settings form
if form_section == "stripe-settings":
# Handle disable action