From d1abd2c450fb1016d152e2cc81856e01da8b03b0 Mon Sep 17 00:00:00 2001 From: yimingl9 Date: Tue, 12 May 2026 19:39:38 -0400 Subject: [PATCH] feat(mywebsite): add new site --- .gitignore | 8 +- Dockerfile | 4 +- control_server.py | 2 +- sites/mega/_health.py | 5 + sites/mega/app.py | 748 ++++++++++ sites/mega/requirements.txt | 4 + sites/mega/seed_data.py | 391 +++++ sites/mega/static/css/.gitkeep | 0 sites/mega/static/css/buttons.css.css | 582 ++++++++ sites/mega/static/css/mega.css | 121 ++ sites/mega/static/css/theme.css.css | 1399 ++++++++++++++++++ sites/mega/static/icons/.gitkeep | 0 sites/mega/static/icons/check-circle-mto.svg | 3 + sites/mega/static/icons/cloud-mto.svg | 3 + sites/mega/static/icons/cloud-upload-mto.svg | 3 + sites/mega/static/icons/code-mro.svg | 3 + sites/mega/static/icons/database-mto.svg | 3 + sites/mega/static/icons/folder-mro.svg | 3 + sites/mega/static/icons/key-01-mto.svg | 4 + sites/mega/static/icons/lock-mto.svg | 3 + sites/mega/static/icons/mega.svg | 4 + sites/mega/static/icons/monitor-mro.svg | 3 + sites/mega/static/icons/password-mto.svg | 3 + sites/mega/static/icons/search-sro.svg | 4 + sites/mega/static/icons/share-mro.svg | 3 + sites/mega/static/icons/shield-mro.svg | 3 + sites/mega/static/icons/sync-mto.svg | 4 + sites/mega/static/icons/transfer-it-mto.svg | 5 + sites/mega/static/icons/users-mro.svg | 3 + sites/mega/static/js/.gitkeep | 0 sites/mega/static/js/mega.js | 6 + sites/mega/tasks.jsonl | 18 + sites/mega/templates/.gitkeep | 0 sites/mega/templates/404.html | 3 + sites/mega/templates/_plan_card.html | 19 + sites/mega/templates/account.html | 33 + sites/mega/templates/account_edit.html | 13 + sites/mega/templates/base.html | 68 + sites/mega/templates/checkout.html | 33 + sites/mega/templates/cloud_drive.html | 42 + sites/mega/templates/contact.html | 12 + sites/mega/templates/download_detail.html | 18 + sites/mega/templates/downloads.html | 21 + sites/mega/templates/file_detail.html | 26 + sites/mega/templates/help.html | 19 + sites/mega/templates/help_article.html | 21 + sites/mega/templates/index.html | 71 + sites/mega/templates/login.html | 13 + sites/mega/templates/order_detail.html | 11 + sites/mega/templates/payment_methods.html | 23 + sites/mega/templates/plan_detail.html | 39 + sites/mega/templates/pricing.html | 32 + sites/mega/templates/product_page.html | 28 + sites/mega/templates/register.html | 14 + sites/mega/templates/search.html | 39 + sites/mega/templates/ticket_detail.html | 14 + sites/mega/templates/vault.html | 33 + websyn_start.sh | 11 +- 58 files changed, 3990 insertions(+), 11 deletions(-) create mode 100644 sites/mega/_health.py create mode 100644 sites/mega/app.py create mode 100644 sites/mega/requirements.txt create mode 100644 sites/mega/seed_data.py create mode 100644 sites/mega/static/css/.gitkeep create mode 100644 sites/mega/static/css/buttons.css.css create mode 100644 sites/mega/static/css/mega.css create mode 100644 sites/mega/static/css/theme.css.css create mode 100644 sites/mega/static/icons/.gitkeep create mode 100644 sites/mega/static/icons/check-circle-mto.svg create mode 100644 sites/mega/static/icons/cloud-mto.svg create mode 100644 sites/mega/static/icons/cloud-upload-mto.svg create mode 100644 sites/mega/static/icons/code-mro.svg create mode 100644 sites/mega/static/icons/database-mto.svg create mode 100644 sites/mega/static/icons/folder-mro.svg create mode 100644 sites/mega/static/icons/key-01-mto.svg create mode 100644 sites/mega/static/icons/lock-mto.svg create mode 100644 sites/mega/static/icons/mega.svg create mode 100644 sites/mega/static/icons/monitor-mro.svg create mode 100644 sites/mega/static/icons/password-mto.svg create mode 100644 sites/mega/static/icons/search-sro.svg create mode 100644 sites/mega/static/icons/share-mro.svg create mode 100644 sites/mega/static/icons/shield-mro.svg create mode 100644 sites/mega/static/icons/sync-mto.svg create mode 100644 sites/mega/static/icons/transfer-it-mto.svg create mode 100644 sites/mega/static/icons/users-mro.svg create mode 100644 sites/mega/static/js/.gitkeep create mode 100644 sites/mega/static/js/mega.js create mode 100644 sites/mega/tasks.jsonl create mode 100644 sites/mega/templates/.gitkeep create mode 100644 sites/mega/templates/404.html create mode 100644 sites/mega/templates/_plan_card.html create mode 100644 sites/mega/templates/account.html create mode 100644 sites/mega/templates/account_edit.html create mode 100644 sites/mega/templates/base.html create mode 100644 sites/mega/templates/checkout.html create mode 100644 sites/mega/templates/cloud_drive.html create mode 100644 sites/mega/templates/contact.html create mode 100644 sites/mega/templates/download_detail.html create mode 100644 sites/mega/templates/downloads.html create mode 100644 sites/mega/templates/file_detail.html create mode 100644 sites/mega/templates/help.html create mode 100644 sites/mega/templates/help_article.html create mode 100644 sites/mega/templates/index.html create mode 100644 sites/mega/templates/login.html create mode 100644 sites/mega/templates/order_detail.html create mode 100644 sites/mega/templates/payment_methods.html create mode 100644 sites/mega/templates/plan_detail.html create mode 100644 sites/mega/templates/pricing.html create mode 100644 sites/mega/templates/product_page.html create mode 100644 sites/mega/templates/register.html create mode 100644 sites/mega/templates/search.html create mode 100644 sites/mega/templates/ticket_detail.html create mode 100644 sites/mega/templates/vault.html diff --git a/.gitignore b/.gitignore index c2efc04..e789923 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,10 @@ sites/*/static/external_cache/ # ============================================================= # Intermediate / volatile — never committed anywhere. # ============================================================= -sites/*/scraped_data/ # scrape pipeline intermediate; runtime data lives in instance_seed/*.db -sites/*/instance/ # rebuilt at every container boot from instance_seed/ +# scrape pipeline intermediate; runtime data lives in instance_seed/*.db +sites/*/scraped_data/ +# rebuilt at every container boot from instance_seed/ +sites/*/instance/ sites/*/venv/ # HF download metadata produced by `hf download`. @@ -92,4 +94,4 @@ secrets.json # ============================================================ # Agent demo results # ============================================================= -agent_demo/runs/ \ No newline at end of file +agent_demo/runs/ diff --git a/Dockerfile b/Dockerfile index 991e5ab..6e4a5c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # WebHarbor — slim, self-contained image. -# 15 Flask mirror sites + control plane on :8101. +# Flask mirror sites + control plane on :8101. FROM python:3.12-slim-bookworm @@ -33,6 +33,6 @@ COPY control_server.py /opt/control_server.py COPY site_runner.py /opt/site_runner.py RUN chmod +x /opt/websyn_start.sh -EXPOSE 8101 40000-40014 +EXPOSE 8101 40000-40015 CMD ["/opt/websyn_start.sh"] diff --git a/control_server.py b/control_server.py index c255253..af921dd 100644 --- a/control_server.py +++ b/control_server.py @@ -26,7 +26,7 @@ 'allrecipes', 'amazon', 'apple', 'arxiv', 'bbc_news', 'booking', 'github', 'google_flights', 'google_map', 'google_search', 'huggingface', 'wolfram_alpha', 'cambridge_dictionary', - 'coursera', 'espn', + 'coursera', 'espn', 'mega', ] BASE_PORT = 40000 WEBSYN_DIR = '/opt/WebSyn' diff --git a/sites/mega/_health.py b/sites/mega/_health.py new file mode 100644 index 0000000..f7b28fd --- /dev/null +++ b/sites/mega/_health.py @@ -0,0 +1,5 @@ +"""Per-site health probe.""" + + +def health(): + return {"ok": True, "site": "mega"} diff --git a/sites/mega/app.py b/sites/mega/app.py new file mode 100644 index 0000000..e94a1e4 --- /dev/null +++ b/sites/mega/app.py @@ -0,0 +1,748 @@ +#!/usr/bin/env python3 +"""MEGA.io mirror — encrypted storage, apps, account, checkout, and help.""" +import json +import os +import re +from datetime import datetime +from functools import wraps + +from flask import ( + Flask, abort, flash, redirect, render_template, request, session, url_for +) +from flask_bcrypt import Bcrypt +from flask_login import ( + LoginManager, UserMixin, current_user, login_required, login_user, logout_user +) +from flask_sqlalchemy import SQLAlchemy + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +DB_PATH = os.path.join(BASE_DIR, "instance", "mega.db") + +app = Flask(__name__, instance_path=os.path.join(BASE_DIR, "instance")) +app.config["SECRET_KEY"] = "webharbor-mega-dev-key" +app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{DB_PATH}" +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + +os.makedirs(os.path.join(BASE_DIR, "instance"), exist_ok=True) + +db = SQLAlchemy(app) +bcrypt = Bcrypt(app) +login_manager = LoginManager(app) +login_manager.login_view = "login" +login_manager.login_message = "Log in to manage your MEGA account." + +STOP_WORDS = { + "the", "a", "an", "of", "in", "on", "at", "to", "for", "with", "and", + "or", "is", "are", "be", "by", "from", "how", "what", "which", "that", + "this", "me", "my", "mega", "plan", "plans", "file", "files" +} + + +class User(db.Model, UserMixin): + __tablename__ = "users" + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + email = db.Column(db.String(140), unique=True, nullable=False, index=True) + password_hash = db.Column(db.String(255), nullable=False) + display_name = db.Column(db.String(120), nullable=False) + phone = db.Column(db.String(40), default="") + company = db.Column(db.String(120), default="") + role = db.Column(db.String(80), default="") + address_line1 = db.Column(db.String(180), default="") + address_line2 = db.Column(db.String(180), default="") + city = db.Column(db.String(90), default="") + state = db.Column(db.String(60), default="") + postal_code = db.Column(db.String(20), default="") + country = db.Column(db.String(80), default="United States") + language = db.Column(db.String(40), default="English") + timezone = db.Column(db.String(80), default="America/Indiana/Indianapolis") + plan_id = db.Column(db.Integer, db.ForeignKey("plans.id"), nullable=True) + storage_used_gb = db.Column(db.Float, default=0) + transfer_used_gb = db.Column(db.Float, default=0) + two_factor_enabled = db.Column(db.Boolean, default=False) + recovery_key_saved = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + plan = db.relationship("Plan", backref="users", lazy=True) + cloud_items = db.relationship("CloudItem", backref="owner", lazy=True, cascade="all, delete-orphan") + vault_items = db.relationship("VaultItem", backref="owner", lazy=True, cascade="all, delete-orphan") + payment_methods = db.relationship("PaymentMethod", backref="user", lazy=True, cascade="all, delete-orphan") + orders = db.relationship("SubscriptionOrder", backref="user", lazy=True, cascade="all, delete-orphan") + tickets = db.relationship("SupportTicket", backref="user", lazy=True, cascade="all, delete-orphan") + + def set_password(self, pw): + self.password_hash = bcrypt.generate_password_hash(pw).decode("utf-8") + + def check_password(self, pw): + return bcrypt.check_password_hash(self.password_hash, pw) + + +class Plan(db.Model): + __tablename__ = "plans" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(120), nullable=False) + slug = db.Column(db.String(120), unique=True, nullable=False, index=True) + audience = db.Column(db.String(60), default="individual") + category = db.Column(db.String(60), default="storage") + monthly_price = db.Column(db.Float, default=0) + yearly_price = db.Column(db.Float, default=0) + storage_tb = db.Column(db.Float, default=0) + transfer_tb = db.Column(db.Float, default=0) + users_included = db.Column(db.Integer, default=1) + vpn_devices = db.Column(db.Integer, default=0) + pass_accounts = db.Column(db.Integer, default=0) + s4_base_tb = db.Column(db.Float, default=0) + active = db.Column(db.Boolean, default=True) + popular = db.Column(db.Boolean, default=False) + tagline = db.Column(db.String(240), default="") + description = db.Column(db.Text, default="") + features = db.Column(db.Text, default="[]") + caveats = db.Column(db.Text, default="[]") + image = db.Column(db.String(240), default="") + + def get_features(self): + return _loads(self.features, []) + + def get_caveats(self): + return _loads(self.caveats, []) + + def annual_savings(self): + if not self.monthly_price: + return 0 + return max(0, round(self.monthly_price * 12 - self.yearly_price, 2)) + + +class ProductPage(db.Model): + __tablename__ = "product_pages" + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(180), nullable=False) + slug = db.Column(db.String(100), unique=True, nullable=False, index=True) + section = db.Column(db.String(80), default="") + summary = db.Column(db.String(300), default="") + body = db.Column(db.Text, default="") + hero_image = db.Column(db.String(240), default="") + highlights = db.Column(db.Text, default="[]") + faq = db.Column(db.Text, default="[]") + nav_order = db.Column(db.Integer, default=100) + + def get_highlights(self): + return _loads(self.highlights, []) + + def get_faq(self): + return _loads(self.faq, []) + + +class Download(db.Model): + __tablename__ = "downloads" + id = db.Column(db.Integer, primary_key=True) + product = db.Column(db.String(80), nullable=False, index=True) + platform = db.Column(db.String(80), nullable=False, index=True) + package_name = db.Column(db.String(120), nullable=False) + version = db.Column(db.String(40), default="") + size_mb = db.Column(db.Float, default=0) + release_date = db.Column(db.String(20), default="") + architecture = db.Column(db.String(60), default="") + checksum = db.Column(db.String(80), default="") + notes = db.Column(db.Text, default="") + recommended = db.Column(db.Boolean, default=False) + icon = db.Column(db.String(240), default="") + + +class HelpArticle(db.Model): + __tablename__ = "help_articles" + id = db.Column(db.Integer, primary_key=True) + category = db.Column(db.String(80), nullable=False, index=True) + title = db.Column(db.String(200), nullable=False) + slug = db.Column(db.String(140), unique=True, nullable=False, index=True) + body = db.Column(db.Text, default="") + applies_to = db.Column(db.String(160), default="") + difficulty = db.Column(db.String(40), default="standard") + updated_at = db.Column(db.String(20), default="2026-05-12") + related_terms = db.Column(db.String(260), default="") + + +class CloudItem(db.Model): + __tablename__ = "cloud_items" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + name = db.Column(db.String(180), nullable=False) + slug = db.Column(db.String(180), nullable=False, index=True) + item_type = db.Column(db.String(30), default="file") + folder = db.Column(db.String(160), default="/") + extension = db.Column(db.String(20), default="") + size_mb = db.Column(db.Float, default=0) + modified_at = db.Column(db.String(20), default="") + sync_status = db.Column(db.String(40), default="Synced") + shared_with = db.Column(db.String(240), default="") + share_link = db.Column(db.String(240), default="") + favorite = db.Column(db.Boolean, default=False) + backup_source = db.Column(db.String(120), default="") + content_summary = db.Column(db.Text, default="") + + +class VaultItem(db.Model): + __tablename__ = "vault_items" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + title = db.Column(db.String(160), nullable=False) + slug = db.Column(db.String(160), nullable=False, index=True) + username = db.Column(db.String(140), default="") + site_url = db.Column(db.String(220), default="") + category = db.Column(db.String(60), default="Login") + strength = db.Column(db.String(40), default="Strong") + last_changed = db.Column(db.String(20), default="") + two_factor = db.Column(db.Boolean, default=False) + notes = db.Column(db.Text, default="") + + +class PaymentMethod(db.Model): + __tablename__ = "payment_methods" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + label = db.Column(db.String(80), default="Personal Visa") + card_type = db.Column(db.String(40), default="Visa") + last4 = db.Column(db.String(4), default="4242") + exp_month = db.Column(db.Integer, default=12) + exp_year = db.Column(db.Integer, default=2028) + billing_country = db.Column(db.String(80), default="United States") + is_default = db.Column(db.Boolean, default=False) + + +class SubscriptionOrder(db.Model): + __tablename__ = "subscription_orders" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + plan_id = db.Column(db.Integer, db.ForeignKey("plans.id"), nullable=False) + order_number = db.Column(db.String(40), unique=True, nullable=False) + billing_cycle = db.Column(db.String(20), default="monthly") + seats = db.Column(db.Integer, default=1) + subtotal = db.Column(db.Float, default=0) + tax = db.Column(db.Float, default=0) + total = db.Column(db.Float, default=0) + status = db.Column(db.String(40), default="active") + created_at = db.Column(db.String(20), default="") + plan = db.relationship("Plan", lazy=True) + + +class SupportTicket(db.Model): + __tablename__ = "support_tickets" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + ticket_number = db.Column(db.String(40), unique=True, nullable=False) + subject = db.Column(db.String(180), nullable=False) + category = db.Column(db.String(80), default="General") + priority = db.Column(db.String(30), default="Normal") + status = db.Column(db.String(40), default="Open") + message = db.Column(db.Text, default="") + created_at = db.Column(db.String(20), default="") + + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + + +def _loads(value, default): + try: + return json.loads(value or "null") or default + except Exception: + return default + + +def slugify(value): + return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-") + + +def tokenize(query): + return [ + token for token in re.split(r"\W+", (query or "").lower()) + if len(token) > 1 and token not in STOP_WORDS + ] + + +def score_text(tokens, *fields): + haystack = " ".join(str(field or "") for field in fields).lower() + score = 0 + for token in tokens: + if token in haystack: + score += 2 + elif any(word.startswith(token) for word in haystack.split()): + score += 1 + return score + + +def scored_search(query, items, fields): + tokens = tokenize(query) + if not tokens: + return list(items) + ranked = [] + for item in items: + score = score_text(tokens, *[getattr(item, field, "") for field in fields]) + if score: + ranked.append((score, item.id, item)) + ranked.sort(key=lambda row: (-row[0], row[1])) + return [item for _, __, item in ranked] + + +def selected_plan(): + slug = session.get("checkout_plan") + return Plan.query.filter_by(slug=slug, active=True).first() if slug else None + + +def current_cart_count(): + return 1 if selected_plan() else 0 + + +def order_total(plan, billing_cycle, seats): + seats = max(1, int(seats or 1)) + if billing_cycle == "yearly": + base = plan.yearly_price + else: + base = plan.monthly_price + subtotal = round(base * seats, 2) + tax = round(subtotal * 0.07, 2) + return subtotal, tax, round(subtotal + tax, 2) + + +def user_files_query(): + return CloudItem.query.filter_by(user_id=current_user.id) + + +def anonymous_allowed(route): + @wraps(route) + def wrapped(*args, **kwargs): + return route(*args, **kwargs) + return wrapped + + +@app.context_processor +def inject_globals(): + return { + "nav_products": ProductPage.query.order_by(ProductPage.nav_order).limit(9).all(), + "cart_plan": selected_plan(), + "cart_count": current_cart_count(), + "image_url": lambda name: url_for("static", filename=f"images/{name}"), + "icon_url": lambda name: url_for("static", filename=f"icons/{name}"), + } + + +@app.template_filter("json") +def json_filter(value): + return _loads(value, []) + + +@app.template_filter("size") +def size_filter(value): + value = float(value or 0) + if value >= 1024: + return f"{value / 1024:.2f} GB" + return f"{value:.1f} MB" + + +@app.route("/") +def index(): + products = ProductPage.query.order_by(ProductPage.nav_order).limit(8).all() + featured_plans = Plan.query.filter_by(active=True).order_by(Plan.popular.desc(), Plan.monthly_price).limit(4).all() + help_articles = HelpArticle.query.order_by(HelpArticle.updated_at.desc()).limit(6).all() + return render_template("index.html", products=products, featured_plans=featured_plans, help_articles=help_articles) + + +@app.route("/pricing") +def pricing(): + audience = request.args.get("audience", "") + category = request.args.get("category", "") + billing = request.args.get("billing", "monthly") + plans_q = Plan.query.filter_by(active=True) + if audience: + plans_q = plans_q.filter_by(audience=audience) + if category: + plans_q = plans_q.filter_by(category=category) + plans = plans_q.order_by(Plan.category, Plan.monthly_price, Plan.storage_tb).all() + return render_template("pricing.html", plans=plans, audience=audience, category=category, billing=billing) + + +@app.route("/plans/") +def plan_detail(slug): + plan = Plan.query.filter_by(slug=slug, active=True).first_or_404() + alternatives = Plan.query.filter(Plan.id != plan.id, Plan.category == plan.category, Plan.active == True).limit(5).all() + return render_template("plan_detail.html", plan=plan, alternatives=alternatives) + + +@app.route("/cart/add/", methods=["POST"]) +def add_to_cart(slug): + plan = Plan.query.filter_by(slug=slug, active=True).first_or_404() + session["checkout_plan"] = plan.slug + session["billing_cycle"] = request.form.get("billing_cycle", "monthly") + session["seats"] = max(1, int(request.form.get("seats", 1) or 1)) + flash(f"{plan.name} is ready for checkout.", "success") + return redirect(url_for("checkout")) + + +@app.route("/cart") +@login_required +def cart(): + return redirect(url_for("checkout")) + + +@app.route("/checkout", methods=["GET", "POST"]) +@login_required +def checkout(): + plan = selected_plan() + if not plan: + flash("Choose a plan before checkout.", "info") + return redirect(url_for("pricing")) + billing = request.form.get("billing_cycle") or session.get("billing_cycle", "monthly") + seats = max(1, int(request.form.get("seats") or session.get("seats", 1) or 1)) + payment_id = request.form.get("payment_id", type=int) + methods = PaymentMethod.query.filter_by(user_id=current_user.id).all() + subtotal, tax, total = order_total(plan, billing, seats) + if request.method == "POST": + if not payment_id: + flash("Select a saved payment method to complete checkout.", "error") + else: + order = SubscriptionOrder( + user_id=current_user.id, + plan_id=plan.id, + order_number=f"MEGA-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}-{current_user.id}", + billing_cycle=billing, + seats=seats, + subtotal=subtotal, + tax=tax, + total=total, + status="active", + created_at=datetime.utcnow().strftime("%Y-%m-%d"), + ) + current_user.plan_id = plan.id + db.session.add(order) + db.session.commit() + session.pop("checkout_plan", None) + flash("Your MEGA subscription is active.", "success") + return redirect(url_for("order_detail", order_number=order.order_number)) + return render_template("checkout.html", plan=plan, methods=methods, billing=billing, seats=seats, subtotal=subtotal, tax=tax, total=total) + + +@app.route("/orders/") +@login_required +def order_detail(order_number): + order = SubscriptionOrder.query.filter_by(user_id=current_user.id, order_number=order_number).first_or_404() + return render_template("order_detail.html", order=order) + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if current_user.is_authenticated: + return redirect(url_for("account")) + if request.method == "POST": + user = User.query.filter_by(email=request.form.get("email", "").strip().lower()).first() + if user and user.check_password(request.form.get("password", "")): + login_user(user) + return redirect(request.args.get("next") or url_for("account")) + flash("Email or password did not match.", "error") + return render_template("login.html") + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + if current_user.is_authenticated: + return redirect(url_for("account")) + if request.method == "POST": + email = request.form.get("email", "").strip().lower() + if User.query.filter_by(email=email).first(): + flash("That email already has an account.", "error") + elif request.form.get("password") != request.form.get("confirm"): + flash("Passwords must match.", "error") + else: + user = User( + username=slugify(email.split("@")[0]), + email=email, + display_name=request.form.get("display_name", "MEGA User"), + recovery_key_saved=False, + ) + user.set_password(request.form.get("password", "")) + db.session.add(user) + db.session.commit() + login_user(user) + return redirect(url_for("account")) + return render_template("register.html") + + +@app.route("/logout") +@login_required +def logout(): + logout_user() + return redirect(url_for("index")) + + +@app.route("/account") +@login_required +def account(): + recent_files = user_files_query().order_by(CloudItem.modified_at.desc()).limit(6).all() + recent_orders = SubscriptionOrder.query.filter_by(user_id=current_user.id).order_by(SubscriptionOrder.id.desc()).limit(5).all() + tickets = SupportTicket.query.filter_by(user_id=current_user.id).order_by(SupportTicket.id.desc()).limit(5).all() + return render_template("account.html", recent_files=recent_files, recent_orders=recent_orders, tickets=tickets) + + +@app.route("/account/edit", methods=["GET", "POST"]) +@login_required +def account_edit(): + if request.method == "POST": + for field in ["display_name", "phone", "company", "role", "address_line1", "address_line2", "city", "state", "postal_code", "country", "language", "timezone"]: + setattr(current_user, field, request.form.get(field, "")) + current_user.two_factor_enabled = bool(request.form.get("two_factor_enabled")) + current_user.recovery_key_saved = bool(request.form.get("recovery_key_saved")) + db.session.commit() + flash("Account details updated.", "success") + return redirect(url_for("account")) + return render_template("account_edit.html") + + +@app.route("/billing/payment", methods=["GET", "POST"]) +@login_required +def payment_methods(): + if request.method == "POST": + last4 = re.sub(r"\D", "", request.form.get("card_number", ""))[-4:] + if len(last4) != 4: + flash("Enter a card number with at least four digits.", "error") + else: + if request.form.get("is_default"): + PaymentMethod.query.filter_by(user_id=current_user.id).update({"is_default": False}) + method = PaymentMethod( + user_id=current_user.id, + label=request.form.get("label", "New card"), + card_type=request.form.get("card_type", "Visa"), + last4=last4, + exp_month=int(request.form.get("exp_month", 12)), + exp_year=int(request.form.get("exp_year", 2029)), + billing_country=request.form.get("billing_country", "United States"), + is_default=bool(request.form.get("is_default")), + ) + db.session.add(method) + db.session.commit() + flash("Payment method saved.", "success") + return redirect(url_for("payment_methods")) + methods = PaymentMethod.query.filter_by(user_id=current_user.id).all() + return render_template("payment_methods.html", methods=methods) + + +@app.route("/cloud") +@app.route("/drive") +@login_required +def cloud_drive(): + q = request.args.get("q", "") + folder = request.args.get("folder", "") + items = user_files_query().all() + if folder: + items = [item for item in items if item.folder == folder] + if q: + items = scored_search(q, items, ["name", "folder", "extension", "content_summary", "shared_with", "backup_source"]) + else: + items = sorted(items, key=lambda item: (item.folder, item.item_type != "folder", item.name.lower())) + folders = sorted({item.folder for item in user_files_query().all()}) + return render_template("cloud_drive.html", items=items, folders=folders, q=q, folder=folder) + + +@app.route("/cloud/new-folder", methods=["POST"]) +@login_required +def new_folder(): + name = request.form.get("name", "").strip() + parent = request.form.get("folder", "/") + if not name: + flash("Folder name is required.", "error") + else: + item = CloudItem(user_id=current_user.id, name=name, slug=slugify(f"{parent}-{name}-{current_user.id}"), item_type="folder", folder=parent, modified_at=datetime.utcnow().strftime("%Y-%m-%d"), content_summary="User-created folder") + db.session.add(item) + db.session.commit() + flash("Folder created.", "success") + return redirect(url_for("cloud_drive", folder=parent)) + + +@app.route("/cloud/upload", methods=["POST"]) +@login_required +def upload_file(): + name = request.form.get("name", "").strip() + folder = request.form.get("folder", "/") + if not name: + flash("File name is required.", "error") + else: + ext = name.rsplit(".", 1)[-1].lower() if "." in name else "" + item = CloudItem( + user_id=current_user.id, + name=name, + slug=slugify(f"{folder}-{name}-{current_user.id}-{datetime.utcnow().timestamp()}"), + item_type="file", + folder=folder, + extension=ext, + size_mb=float(request.form.get("size_mb", 12) or 12), + modified_at=datetime.utcnow().strftime("%Y-%m-%d"), + sync_status="Synced", + content_summary=request.form.get("content_summary", "Uploaded through the MEGA web client."), + ) + db.session.add(item) + current_user.storage_used_gb += item.size_mb / 1024 + db.session.commit() + flash("Upload added to Cloud drive.", "success") + return redirect(url_for("cloud_drive", folder=folder)) + + +@app.route("/cloud/item/", methods=["GET", "POST"]) +@login_required +def file_detail(slug): + item = CloudItem.query.filter_by(user_id=current_user.id, slug=slug).first_or_404() + if request.method == "POST": + item.shared_with = request.form.get("shared_with", item.shared_with) + item.favorite = bool(request.form.get("favorite")) + if request.form.get("create_link") and not item.share_link: + item.share_link = f"https://mega.nz/file/{item.slug[:10].upper()}#webharbor" + db.session.commit() + flash("Sharing settings updated.", "success") + return redirect(url_for("file_detail", slug=item.slug)) + return render_template("file_detail.html", item=item) + + +@app.route("/pass") +def pass_page(): + page = ProductPage.query.filter_by(slug="pass").first_or_404() + return render_template("product_page.html", page=page) + + +@app.route("/vault", methods=["GET", "POST"]) +@login_required +def vault(): + if request.method == "POST": + item = VaultItem( + user_id=current_user.id, + title=request.form.get("title", "").strip(), + slug=slugify(f"{request.form.get('title', '')}-{current_user.id}-{datetime.utcnow().timestamp()}"), + username=request.form.get("username", ""), + site_url=request.form.get("site_url", ""), + category=request.form.get("category", "Login"), + strength=request.form.get("strength", "Strong"), + last_changed=datetime.utcnow().strftime("%Y-%m-%d"), + two_factor=bool(request.form.get("two_factor")), + notes=request.form.get("notes", ""), + ) + if not item.title: + flash("Vault entry title is required.", "error") + else: + db.session.add(item) + db.session.commit() + flash("Vault entry saved.", "success") + return redirect(url_for("vault")) + q = request.args.get("q", "") + items = VaultItem.query.filter_by(user_id=current_user.id).all() + if q: + items = scored_search(q, items, ["title", "username", "site_url", "category", "notes", "strength"]) + return render_template("vault.html", items=items, q=q) + + +@app.route("/downloads") +def downloads(): + product = request.args.get("product", "") + platform = request.args.get("platform", "") + q = Download.query + if product: + q = q.filter_by(product=product) + if platform: + q = q.filter_by(platform=platform) + downloads_list = q.order_by(Download.product, Download.platform, Download.recommended.desc()).all() + return render_template("downloads.html", downloads=downloads_list, product=product, platform=platform) + + +@app.route("/downloads/") +def download_detail(download_id): + download = Download.query.get_or_404(download_id) + return render_template("download_detail.html", download=download) + + +@app.route("/help") +def help_center(): + q = request.args.get("q", "") + category = request.args.get("category", "") + articles = HelpArticle.query.all() + if category: + articles = [a for a in articles if a.category == category] + if q: + articles = scored_search(q, articles, ["title", "body", "applies_to", "related_terms", "category"]) + categories = sorted({a.category for a in HelpArticle.query.all()}) + return render_template("help.html", articles=articles, categories=categories, q=q, category=category) + + +@app.route("/help/") +def help_article(slug): + article = HelpArticle.query.filter_by(slug=slug).first_or_404() + related = scored_search(article.related_terms, HelpArticle.query.filter(HelpArticle.id != article.id).all(), ["title", "body", "category"])[:4] + return render_template("help_article.html", article=article, related=related) + + +@app.route("/contact", methods=["GET", "POST"]) +@login_required +def contact(): + if request.method == "POST": + subject = request.form.get("subject", "").strip() + message = request.form.get("message", "").strip() + if not subject or not message: + flash("Subject and message are required.", "error") + else: + ticket = SupportTicket( + user_id=current_user.id, + ticket_number=f"MEGA-T{datetime.utcnow().strftime('%m%d%H%M%S')}{current_user.id}", + subject=subject, + category=request.form.get("category", "General"), + priority=request.form.get("priority", "Normal"), + status="Open", + message=message, + created_at=datetime.utcnow().strftime("%Y-%m-%d"), + ) + db.session.add(ticket) + db.session.commit() + flash("Support ticket submitted.", "success") + return redirect(url_for("ticket_detail", ticket_number=ticket.ticket_number)) + return render_template("contact.html") + + +@app.route("/support/tickets/") +@login_required +def ticket_detail(ticket_number): + ticket = SupportTicket.query.filter_by(user_id=current_user.id, ticket_number=ticket_number).first_or_404() + return render_template("ticket_detail.html", ticket=ticket) + + +@app.route("/search") +def search(): + q = request.args.get("q", "") + plans = scored_search(q, Plan.query.filter_by(active=True).all(), ["name", "audience", "category", "tagline", "description", "features"])[:10] + pages = scored_search(q, ProductPage.query.all(), ["title", "section", "summary", "body", "highlights"])[:10] + articles = scored_search(q, HelpArticle.query.all(), ["title", "body", "applies_to", "related_terms", "category"])[:10] + downloads_found = scored_search(q, Download.query.all(), ["product", "platform", "package_name", "version", "architecture", "notes"])[:10] + files = [] + vault_items = [] + if current_user.is_authenticated: + files = scored_search(q, user_files_query().all(), ["name", "folder", "extension", "content_summary", "shared_with", "backup_source"])[:10] + vault_items = scored_search(q, VaultItem.query.filter_by(user_id=current_user.id).all(), ["title", "username", "site_url", "category", "notes", "strength"])[:10] + return render_template("search.html", q=q, plans=plans, pages=pages, articles=articles, downloads=downloads_found, files=files, vault_items=vault_items) + + +@app.route("/") +def product_page(slug): + page = ProductPage.query.filter_by(slug=slug).first() + if not page: + abort(404) + return render_template("product_page.html", page=page) + + +@app.route("/_health") +def health(): + return {"ok": True, "site": "mega", "plans": Plan.query.count(), "users": User.query.count()} + + +with app.app_context(): + db.create_all() + from seed_data import seed_benchmark_users, seed_database + + seed_database() + seed_benchmark_users() + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 5000)) + app.run(host="0.0.0.0", port=port, debug=False) diff --git a/sites/mega/requirements.txt b/sites/mega/requirements.txt new file mode 100644 index 0000000..d98a374 --- /dev/null +++ b/sites/mega/requirements.txt @@ -0,0 +1,4 @@ +Flask +Flask-SQLAlchemy +Flask-Login +Flask-Bcrypt diff --git a/sites/mega/seed_data.py b/sites/mega/seed_data.py new file mode 100644 index 0000000..d3582ab --- /dev/null +++ b/sites/mega/seed_data.py @@ -0,0 +1,391 @@ +"""Deterministic seed data for the MEGA mirror.""" +import json +from datetime import datetime + +from app import ( + CloudItem, Download, HelpArticle, PaymentMethod, Plan, ProductPage, + SubscriptionOrder, SupportTicket, User, VaultItem, db, slugify +) + +PASSWORD = "TestPass123!" +SEED_DATE = "2026-05-12" + + +def dumps(value): + return json.dumps(value, sort_keys=True) + + +def seed_database(): + if Plan.query.count() > 0: + return + + plans = [ + ("Free", "free", "individual", "storage", 0, 0, 0.02, 0.10, 1, 0, 0, 0, False, + "Start with encrypted storage", + "A no-cost account for basic encrypted storage, sharing, and testing MEGA apps.", + ["20 GB starting storage", "Encrypted file sharing", "Mobile and desktop apps", "Basic transfer quota"], ["Transfer quota is limited"]), + ("Pro Lite", "pro-lite", "individual", "storage", 5.49, 54.99, 0.40, 1.00, 1, 5, 1, 0, False, + "Extra space for everyday files", + "A compact plan for personal documents, phone photos, and a small password vault.", + ["400 GB storage", "1 TB transfer", "MEGA Pass included", "VPN on 5 devices"], ["Best for one person"]), + ("Pro I", "pro-i", "individual", "storage", 10.99, 109.99, 2.00, 2.00, 1, 10, 1, 0, True, + "More room for photos and projects", + "Balanced encrypted cloud storage with enough transfer for active personal workflows.", + ["2 TB storage", "2 TB transfer", "10 VPN devices", "Password manager", "Priority email support"], []), + ("Pro II", "pro-ii", "individual", "storage", 21.99, 219.99, 8.00, 8.00, 1, 10, 1, 0, False, + "Large encrypted storage for creators", + "A larger plan for video archives, client project files, and recurring backups.", + ["8 TB storage", "8 TB transfer", "File versioning", "MEGA Pass", "MEGA VPN"], []), + ("Pro III", "pro-iii", "individual", "storage", 32.99, 329.99, 16.00, 16.00, 1, 10, 1, 0, False, + "Maximum personal capacity", + "High-capacity encrypted storage for heavy archives and large media libraries.", + ["16 TB storage", "16 TB transfer", "Large shared links", "Priority support", "Advanced recovery tools"], []), + ("VPN Monthly", "vpn-monthly", "individual", "vpn", 3.99, 0, 0, 0, 1, 10, 0, 0, False, + "Private browsing month to month", + "Standalone VPN access with ad blocking, unsafe network detection, and a kill switch.", + ["10 devices", "Unlimited VPN traffic", "Ad blocking", "Kill switch", "Automatic unsafe network protection"], []), + ("VPN Annual", "vpn-annual", "individual", "vpn", 2.34, 28.08, 0, 0, 1, 10, 0, 0, True, + "Lowest VPN monthly equivalent", + "Annual VPN access for users who want the lowest monthly equivalent price.", + ["10 devices", "Unlimited traffic", "Always-on protection", "City-level locations", "Ad blocking"], ["Charged annually"]), + ("Pass Free", "pass-free", "individual", "pass", 0, 0, 0, 0, 1, 0, 1, 0, False, + "Secure password storage", + "MEGA Pass for a starter vault with encrypted login storage.", + ["Encrypted vault", "Password generator", "Autofill", "Device sync"], ["Limited sharing"]), + ("Pass Pro", "pass-pro", "individual", "pass", 2.99, 29.99, 0, 0, 1, 0, 1, 0, False, + "Unlimited password protection", + "Full MEGA Pass with unlimited vault entries, secure notes, and sharing.", + ["Unlimited passwords", "Secure notes", "Password health", "One-time password support", "Vault export"], []), + ("Business Starter", "business-starter", "business", "business", 15.00, 150.00, 3.00, 3.00, 3, 10, 3, 0, False, + "Small team storage", + "A team plan for startups that need encrypted storage, user management, and shared folders.", + ["3 users included", "3 TB pooled storage", "Admin dashboard", "Shared folders", "Team chat"], []), + ("Business Pro", "business-pro", "business", "business", 24.00, 240.00, 10.00, 10.00, 5, 10, 5, 0, True, + "Team collaboration with room to grow", + "Expanded team storage with stronger administration, compliance exports, and priority support.", + ["5 users included", "10 TB pooled storage", "User management dashboard", "External collaborator controls", "Priority support"], []), + ("Business Enterprise", "business-enterprise", "business", "business", 40.00, 400.00, 25.00, 25.00, 10, 10, 10, 0, False, + "Large-team governance", + "Encrypted collaboration for larger teams with advanced support and account governance.", + ["10 users included", "25 TB pooled storage", "Advanced account recovery", "Audit-ready exports", "Dedicated onboarding"], []), + ("S4 Fixed Storage", "s4-fixed-storage", "business", "objectstorage", 19.99, 199.99, 3.00, 15.00, 1, 0, 0, 3, False, + "Predictable S3-compatible storage", + "Object storage for backups and media libraries with zero surprise egress on typical workloads.", + ["3 TB base object storage", "S3-compatible API", "5x included egress guide", "Lifecycle-friendly archive use"], []), + ("Pro Flexi", "pro-flexi", "business", "objectstorage", 16.00, 160.00, 3.00, 3.00, 1, 10, 1, 3, False, + "Pay as you grow", + "Flexible base storage and transfer for variable cloud workloads and S4 experiments.", + ["3 TB base storage and transfer", "Additional TB billing", "MEGA VPN", "MEGA Pass", "Priority support"], ["Usage can vary monthly"]), + ("S4 Media Vault", "s4-media-vault", "business", "objectstorage", 29.00, 290.00, 6.00, 18.00, 1, 0, 0, 6, False, + "Media archive object storage", + "A media-focused object storage plan for teams keeping large video libraries online.", + ["6 TB object storage", "Media lifecycle workflows", "Preview-friendly archive structure", "Predictable monthly billing"], []), + ("S4 Analytics Reserve", "s4-analytics-reserve", "business", "objectstorage", 34.00, 340.00, 8.00, 12.00, 1, 0, 0, 8, False, + "Data lake reserve capacity", + "Object storage for analytics exports and model training sets with steady retention needs.", + ["8 TB object storage", "Analytics export staging", "Retention labels", "Team access policies"], []), + ("S4 Developer Sandbox", "s4-developer-sandbox", "business", "objectstorage", 9.00, 90.00, 1.00, 2.00, 1, 0, 0, 1, False, + "Small object storage sandbox", + "A lower-capacity object storage plan for development tests and automation rehearsals.", + ["1 TB object storage", "Test automation workflows", "Temporary datasets", "Developer-friendly setup"], []), + ("S4 Enterprise Archive", "s4-enterprise-archive", "business", "objectstorage", 58.00, 580.00, 20.00, 30.00, 1, 0, 0, 20, False, + "Long-retention archive storage", + "Large object storage capacity for compliance archives and disaster recovery copies.", + ["20 TB object storage", "Archive policy planning", "Disaster recovery copies", "Priority onboarding"], []), + ("Backup Reserve", "backup-reserve", "business", "objectstorage", 12.00, 120.00, 2.00, 4.00, 1, 0, 0, 2, False, + "Backup-focused storage pool", + "A near-miss storage option for backup teams that need object-style retention but less transfer.", + ["2 TB retained backups", "Desktop backup staging", "Version history review", "Recovery planning"], []), + ] + for row in plans: + db.session.add(Plan( + name=row[0], slug=row[1], audience=row[2], category=row[3], + monthly_price=row[4], yearly_price=row[5], storage_tb=row[6], + transfer_tb=row[7], users_included=row[8], vpn_devices=row[9], + pass_accounts=row[10], s4_base_tb=row[11], popular=row[12], + tagline=row[13], description=row[14], features=dumps(row[15]), + caveats=dumps(row[16]), image="logo-mega.png", + )) + + pages = [ + ("Cloud storage", "storage", "Products", "Securely store, manage, and share encrypted files online.", "Feature-1.png", + ["Encrypted storage by default", "Fast downloads and uploads", "Mobile and desktop access", "File and folder links"]), + ("Business", "business", "Business", "Encrypted team storage, user management, and project collaboration.", "Business-img-1.png", + ["Team dashboard", "Shared folders", "Video meetings", "Client-safe file exchange"]), + ("Object storage", "objectstorage", "Products", "S3-compatible storage for backup, media, analytics, and automation.", "True-Nas-Community-edition.png", + ["S3-compatible API", "Predictable pricing", "Backup and archiving", "Media and ML workloads"]), + ("VPN", "vpn", "Products", "Private browsing with ad blocking, unsafe network detection, and a kill switch.", "MEGA-VPN_Locations.png", + ["10 devices", "Unlimited VPN traffic", "Unsafe network detection", "Kill switch"]), + ("Password manager", "pass", "Products", "Encrypted password vault, autofill, generator, and one-time-password support.", "pass-hero.png", + ["Encrypted vault", "Password health", "OTP support", "Secure notes"]), + ("Transfer.it", "transfer-it", "Products", "Fast, simple, and secure file transfers powered by MEGA.", "transfer-it-hero-1.png", + ["Generous limits", "Frictionless performance", "Link-based transfers", "MEGA account controls"]), + ("Desktop app", "desktop", "Apps", "Windows, macOS, and Linux app for sync, backup, and transfer control.", "DA-img-2.png", + ["Selective sync", "Backup folders", "Transfer manager", "Windows, macOS, Linux"]), + ("Mobile apps", "mobile", "Apps", "Android and iOS cloud storage, camera uploads, scan, print, and offline files.", "Mobile-img-2.png", + ["Camera uploads", "Offline access", "Document scan", "Slideshow viewing"]), + ("MEGA CMD", "cmd", "Apps", "Command-line access to MEGA services for automation and advanced workflows.", "20230215_Mega_icons_upd_00017.png", + ["Interactive shell", "Scriptable commands", "QNAP and Synology packages", "Build documentation"]), + ("Sync", "syncing", "Features", "Automated synchronisation across devices and shared folders.", "Sync-img-1-1.png", + ["Shared-folder sync", "Selective sync", "Version handling", "Secure transport"]), + ("Backup", "megabackup", "Features", "Back up and recover important files with visibility across devices.", "Backup-2.png", + ["Device backups", "Recovery history", "Ransomware recovery", "Phone photo backups"]), + ("Share", "share", "Features", "Secure sharing with links, contacts, upload folders, and chat collaboration.", "share-1.png", + ["Folder sharing", "Upload requests", "Chat collaboration", "Link controls"]), + ("Security and privacy", "security", "Company", "Zero-knowledge encryption, recovery keys, 2FA, and transparent security design.", "Sec-img-1.png", + ["End-to-end encryption", "Recovery key", "2FA", "Ransomware recovery"]), + ("Developers", "developers", "Resources", "MEGA SDK and developer documentation for integrating MEGA client access.", "20230215_Mega_icons_upd_00003.png", + ["Core SDK", "C++ client engine", "API access model", "Open source references"]), + ("Freelancers", "freelancers", "Solutions", "Encrypted storage and transfer workflows for independent professionals.", "MEGA-icon-cloud.png", + ["Client delivery", "Password vault", "Project backups", "Shared folders"]), + ("Small business", "small-business", "Solutions", "Storage, backups, chat, and external collaboration for small teams.", "Business-img-3.png", + ["Admin dashboard", "Team folders", "Backup policies", "Client uploads"]), + ("Media files", "media-files", "Solutions", "Large media previews, archives, delivery links, and encrypted collaboration.", "Media-and-video-storage.png", + ["Video archive", "Preview links", "Transfer capacity", "Version recovery"]), + ] + for order, (title, slug, section, summary, image, highlights) in enumerate(pages, 1): + body = ( + f"{title} on MEGA focuses on private, encrypted workflows. " + f"The mirror keeps the same navigation and product framing while adding local forms and account data for benchmark tasks." + ) + faq = [ + {"q": f"Who is {title} for?", "a": summary}, + {"q": "Does this work with a MEGA account?", "a": "Yes. Authenticated routes in this mirror persist to the local SQLite database."}, + ] + db.session.add(ProductPage( + title=title, slug=slug, section=section, summary=summary, body=body, + hero_image=image, highlights=dumps(highlights), faq=dumps(faq), nav_order=order + )) + + downloads = [ + ("Desktop", "Windows", "MEGAsyncSetup64.exe", "5.12.1", 83.4, "x64", True, "Windows-1.png", "Sync, backup, and transfer manager for Windows."), + ("Desktop", "Windows", "MEGAcmdSetup64.exe", "1.7.2", 57.2, "x64", False, "Windows-2.png", "Command-line package for Windows automation."), + ("Desktop", "Windows", "MEGAsyncSetup32.exe", "5.12.1", 79.2, "x86", False, "Windows-1.png", "Legacy Windows sync package for 32-bit systems."), + ("Desktop", "Windows", "MEGAsyncARM64.exe", "5.12.1", 81.6, "ARM64", False, "Windows-2.png", "Windows ARM64 sync package."), + ("CMD", "Windows", "MEGAcmdPortable.zip", "1.7.2", 49.8, "Portable x64", False, "20230215_Mega_icons_upd_00017.png", "Portable command-line archive for Windows automation."), + ("Pass", "Windows", "MEGAPassDesktop.exe", "1.12.4", 58.6, "x64", False, "password-icon.png", "Desktop password manager companion for Windows."), + ("Desktop", "macOS", "MEGAsync-macOS.dmg", "5.12.1", 96.1, "Apple silicon + Intel", True, "MacOS-1.png", "Desktop sync and backup for macOS."), + ("Desktop", "Linux", "megasync-x86_64.deb", "5.12.1", 72.8, "Debian/Ubuntu", True, "DA-img-3.png", "Linux desktop client for sync and transfer control."), + ("Mobile", "Android", "MEGA Android app", "14.8", 64.0, "ARM", True, "Android-1.png", "Camera uploads, offline files, and document scan."), + ("Mobile", "iOS", "MEGA iOS app", "14.8", 71.5, "Universal", True, "iOS-1.png", "Camera uploads, slideshow, live text detection, and offline files."), + ("VPN", "Windows", "MEGAvpnSetup64.exe", "2.3.0", 41.2, "x64", True, "Picture-3vpn.png", "MEGA VPN with kill switch and ad blocking."), + ("VPN", "macOS", "MEGA VPN.dmg", "2.3.0", 46.5, "Apple silicon + Intel", True, "MEGA-VPN_Locations.png", "VPN client for macOS."), + ("VPN", "Android", "MEGA VPN Android", "2.3.0", 38.2, "ARM", False, "Android-2.png", "Mobile VPN protection."), + ("VPN", "iOS", "MEGA VPN iOS", "2.3.0", 39.0, "Universal", False, "iOS-2.png", "iOS VPN protection."), + ("Pass", "Chrome", "MEGA Pass Chrome extension", "1.12.4", 9.2, "Browser", True, "icon-Chrome.png", "Autofill and password vault extension."), + ("Pass", "Firefox", "MEGA Pass Firefox extension", "1.12.4", 9.4, "Browser", False, "firefox_logo_platform.png", "Firefox extension for MEGA Pass."), + ("Pass", "Edge", "MEGA Pass Edge extension", "1.12.4", 9.3, "Browser", False, "icon-Microsoft-Edge.png", "Edge extension for MEGA Pass."), + ("CMD", "QNAP", "MEGAcmd_QNAP.qpkg", "1.7.2", 65.3, "NAS", False, "20230215_Mega_icons_upd_00017.png", "QNAP package for command automation."), + ("CMD", "Synology", "MEGAcmd_Synology.spk", "1.7.2", 62.9, "NAS", False, "Synology-logo.png", "Synology package for command automation."), + ] + for product, platform, package, version, size, architecture, recommended, icon, notes in downloads: + db.session.add(Download( + product=product, platform=platform, package_name=package, version=version, + size_mb=size, release_date=SEED_DATE, architecture=architecture, + checksum=f"sha256-{slugify(package)[:18]}-webharbor", notes=notes, + recommended=recommended, icon=icon + )) + + article_specs = [ + ("Account", "Save your recovery key", "Your recovery key is the safety net for a forgotten password. Download it, store it offline, and do not share it with support or teammates.", "recovery key account password backup"), + ("Account", "Enable two-factor authentication", "Two-factor authentication adds a second verification step. Use an authenticator app and keep backup codes outside the vault.", "2fa authenticator security login"), + ("Account", "Change account email address", "Verify the new email address before removing the old one. Team admins should confirm the user still appears in the dashboard.", "email profile account"), + ("Cloud drive", "Create a shared folder", "Use folder sharing when collaborators need ongoing access. Choose read-only, read-and-write, or full access depending on the project.", "shared folder permissions collaboration"), + ("Cloud drive", "Create a file link", "File links can be created from the detail menu. Add a decryption key only when the recipient is trusted.", "file link share key"), + ("Cloud drive", "Find large files", "Sort by size or search by extension to identify large media archives before changing plans.", "large files storage quota search"), + ("Cloud drive", "Recover deleted files", "Recovery depends on file versioning and retention. Check the backup source and modified date before restoring.", "restore deleted file ransomware"), + ("Sync", "Set up selective sync", "Selective sync keeps chosen folders on each device while leaving the rest in the cloud.", "selective sync desktop folders"), + ("Sync", "Resolve sync conflicts", "Conflicted copies show the device and timestamp. Keep the latest verified edit and archive the extra copy.", "sync conflict desktop"), + ("Backup", "Add a desktop backup", "Choose folders from the desktop app, name the backup source, and confirm it appears in Cloud drive.", "backup desktop source"), + ("Backup", "Recover from ransomware", "Disconnect the affected device, review version history, and restore a clean version from before the incident.", "ransomware recovery version history"), + ("Mobile", "Turn on camera uploads", "Camera uploads can include photos and videos. Confirm the destination folder and mobile data preference.", "camera uploads mobile photos"), + ("Mobile", "Make files available offline", "Mark files for offline access from the mobile app; they remain encrypted on the device.", "offline access mobile"), + ("Pass", "Import passwords from a browser", "Export a CSV from the browser, import it into MEGA Pass, then delete the CSV after confirming entries.", "password import browser csv"), + ("Pass", "Use one-time passwords", "MEGA Pass can store one-time-password seeds for accounts that support two-factor authentication.", "otp password manager two factor"), + ("Pass", "Find weak passwords", "Password health highlights reused or weak entries. Update the oldest weak entries first.", "weak password health reused"), + ("VPN", "Use automatic VPN on unsafe networks", "Automatic protection detects untrusted networks and starts the VPN without another click.", "unsafe network automatic vpn"), + ("VPN", "Understand the kill switch", "The kill switch blocks internet traffic if the VPN tunnel drops, preventing accidental exposure.", "kill switch vpn privacy"), + ("VPN", "Choose a VPN city", "Pick the nearest city for speed or a specific region for a location-sensitive workflow.", "vpn city location"), + ("Object storage", "Estimate S4 costs", "S4 planning starts with stored terabytes and egress. Included egress is based on typical usage ratios.", "s4 object storage egress cost"), + ("Object storage", "Connect S3 tools", "Use S3-compatible access keys with tools such as Rclone, Duplicacy, TrueNAS, and S3 Browser.", "s3 compatible rclone truenas duplicacy"), + ("Object storage", "Plan backup and archiving", "Object storage works well for long-term archives, disaster recovery, and media libraries.", "backup archiving s4"), + ("Business", "Invite a team member", "Admins invite users from the team dashboard and assign access to shared folders after acceptance.", "team invite admin dashboard"), + ("Business", "Remove external collaborator access", "Review shared folders, remove the collaborator, and rotate links if they had download access.", "external collaborator remove access"), + ("Business", "Review team storage usage", "The dashboard separates pooled storage, transfer, and backup sources so admins can plan upgrades.", "team storage usage dashboard"), + ("Business", "Export a compliance summary", "Export account, device, and shared-folder summaries for internal audit workflows.", "compliance export audit"), + ("Transfer.it", "Send a large transfer", "Transfer.it is for simple link-based delivery when a persistent shared folder is unnecessary.", "large transfer link"), + ("Transfer.it", "Manage transfer expiry", "Set an expiry date for sensitive transfers and resend a new link if the recipient misses it.", "transfer expiry link"), + ("Downloads", "Pick the right desktop package", "Windows users usually need MEGAsyncSetup64.exe; automation users may need MEGAcmdSetup64.exe.", "desktop windows cmd package"), + ("Downloads", "Install CMD on NAS", "QNAP and Synology packages are available for NAS automation workflows.", "cmd qnap synology nas"), + ("Security", "Understand zero-knowledge encryption", "MEGA cannot reset an account password without the user's recovery key because file keys are controlled by the user.", "zero knowledge encryption privacy"), + ("Security", "Share safely with decryption keys", "Treat decryption keys like passwords and send them through a separate channel when needed.", "decryption key share"), + ("Developers", "Use the MEGA SDK", "The SDK provides the client access engine and keeps encryption behavior consistent across integrations.", "sdk developers client engine"), + ("Developers", "Automate with MEGA CMD", "MEGA CMD supports scripted uploads, downloads, sync checks, and account status commands.", "cmd automate commands"), + ("Billing", "Switch from monthly to yearly", "Yearly billing uses the annual price and can reduce the monthly equivalent for eligible plans.", "billing yearly monthly savings"), + ("Billing", "Update a payment method", "Add the new card, mark it default, then remove the old card after the next successful renewal.", "payment method card billing"), + ] + for category, title, body, terms in article_specs: + db.session.add(HelpArticle( + category=category, title=title, slug=slugify(title), body=body, + applies_to=category, difficulty="standard", updated_at=SEED_DATE, + related_terms=terms + )) + + db.session.commit() + + +def seed_benchmark_users(): + if User.query.filter_by(email="alice.j@test.com").first(): + return + + plan_by_slug = {p.slug: p for p in Plan.query.all()} + users = [ + ("alice_j", "alice.j@test.com", "Alice Johnson", "Riverlight Studio", "Creative Director", "pro-ii", 5240, 1810, True, True), + ("bob_c", "bob.c@test.com", "Bob Chen", "Northstar Analytics", "Data Engineer", "pro-flexi", 2320, 980, True, False), + ("carol_d", "carol.d@test.com", "Carol Davis", "Cedar Legal Group", "Operations Manager", "business-pro", 7710, 2450, False, True), + ("david_k", "david.k@test.com", "David Kim", "David Kim Photo", "Freelancer", "pro-i", 1180, 540, True, True), + ] + user_objs = {} + for username, email, display, company, role, plan_slug, storage, transfer, two_factor, recovery in users: + user = User( + username=username, email=email, display_name=display, company=company, role=role, + phone="317-555-0142", address_line1="415 Market Street", address_line2="Suite 8", + city="Indianapolis", state="IN", postal_code="46204", country="United States", + language="English", timezone="America/Indiana/Indianapolis", + plan_id=plan_by_slug[plan_slug].id, storage_used_gb=storage, transfer_used_gb=transfer, + two_factor_enabled=two_factor, recovery_key_saved=recovery + ) + user.set_password(PASSWORD) + db.session.add(user) + user_objs[email] = user + db.session.flush() + + for user in user_objs.values(): + db.session.add_all([ + PaymentMethod(user_id=user.id, label="Personal Visa", card_type="Visa", last4="4242", exp_month=12, exp_year=2028, billing_country="United States", is_default=True), + PaymentMethod(user_id=user.id, label="Backup Mastercard", card_type="Mastercard", last4="1881", exp_month=9, exp_year=2029, billing_country="United States", is_default=False), + ]) + + alice = user_objs["alice.j@test.com"] + bob = user_objs["bob.c@test.com"] + carol = user_objs["carol.d@test.com"] + david = user_objs["david.k@test.com"] + + def add_files(user, specs): + for name, folder, item_type, size, modified, sync_status, shared, favorite, backup, summary in specs: + db.session.add(CloudItem( + user_id=user.id, name=name, slug=slugify(f"{user.username}-{folder}-{name}"), + item_type=item_type, folder=folder, extension=(name.rsplit(".", 1)[-1].lower() if "." in name else ""), + size_mb=size, modified_at=modified, sync_status=sync_status, shared_with=shared, + share_link=(f"https://mega.nz/file/{slugify(name)[:10].upper()}#seeded" if shared else ""), + favorite=favorite, backup_source=backup, content_summary=summary + )) + + common_distractors = [ + ("Travel itinerary.pdf", "/Documents", "file", 3.4, "2026-04-22", "Synced", "", False, "", "Personal itinerary and bookings."), + ("Old export passwords.csv", "/Security", "file", 0.7, "2025-10-02", "Synced", "", False, "", "Legacy password export retained for audit only."), + ("Camera Uploads 2026.zip", "/Backups/Phone", "file", 884.0, "2026-05-01", "Synced", "david.k@test.com", False, "iPhone 15 Pro", "Photo and video backup archive."), + ("Shared client intake.docx", "/Client Vault", "file", 2.1, "2026-04-18", "Synced", "carol.d@test.com", True, "", "Client intake form and checklist."), + ("Tax receipts archive.zip", "/Finance", "file", 76.4, "2026-03-18", "Synced", "", False, "", "Annual tax receipt package."), + ("Conference slides.pptx", "/Documents", "file", 44.9, "2026-04-02", "Synced", "", False, "", "Presentation slides for conference talk."), + ("Device backup summary.pdf", "/Backups", "file", 2.8, "2026-05-05", "Synced", "", False, "Desktop app", "Backup status exported from desktop app."), + ("Shared media preview.mp4", "/Media Archive", "file", 520.0, "2026-04-25", "Synced", "alice.j@test.com", False, "", "Preview video shared with collaborators."), + ("Project notes.txt", "/Projects", "file", 0.2, "2026-05-03", "Synced", "", False, "", "General project notes."), + ("Recovery checklist.pdf", "/Security", "file", 1.5, "2026-04-10", "Synced", "", True, "", "Account recovery and incident-response checklist."), + ] + add_files(alice, [ + ("Brand refresh brief.pdf", "/Projects/Atlas", "file", 18.6, "2026-05-08", "Synced", "bob.c@test.com", True, "", "Creative brief for Atlas rebrand."), + ("Atlas launch footage.mov", "/Media Archive", "file", 4380.0, "2026-05-06", "Synced", "bob.c@test.com, carol.d@test.com", False, "MacBook Pro", "Long-form 4K launch footage for Atlas."), + ("Atlas social cuts.zip", "/Media Archive", "file", 1260.0, "2026-05-07", "Syncing", "bob.c@test.com", False, "MacBook Pro", "Compressed short clips for social launch."), + ("Q2 invoice batch.xlsx", "/Finance", "file", 9.8, "2026-04-30", "Synced", "carol.d@test.com", True, "", "Quarterly invoices and payment status."), + ("MEGA recovery key.pdf", "/Security", "file", 0.4, "2026-01-15", "Synced", "", True, "", "Account recovery key saved offline."), + ("Client testimonials raw.wav", "/Media Archive", "file", 740.5, "2026-05-03", "Synced", "", False, "Studio NAS", "Audio interviews for testimonial edit."), + ("Atlas b-roll selects.mov", "/Media Archive", "file", 980.0, "2026-05-05", "Synced", "", False, "MacBook Pro", "B-roll clips for Atlas launch."), + ("Atlas color grade preview.mp4", "/Media Archive", "file", 610.0, "2026-05-04", "Synced", "bob.c@test.com", False, "MacBook Pro", "Compressed color grade preview."), + ("Podcast sponsor reel.mov", "/Media Archive", "file", 1320.0, "2026-04-27", "Synced", "", False, "Studio NAS", "Sponsor reel export."), + ("Product stills archive.zip", "/Media Archive", "file", 980.0, "2026-04-19", "Synced", "carol.d@test.com", False, "", "Product photography archive."), + ("Iceland selects 01.zip", "/Photos/Iceland", "file", 1860.0, "2026-02-18", "Synced", "david.k@test.com", True, "iPhone 15 Pro", "Edited Iceland travel photos."), + ("Iceland selects 02.zip", "/Photos/Iceland", "file", 1744.0, "2026-02-20", "Synced", "", False, "iPhone 15 Pro", "Alternate photo set."), + ("Studio insurance policy.pdf", "/Documents", "file", 5.5, "2026-03-11", "Synced", "", False, "", "Business insurance policy."), + ("Ransomware drill notes.md", "/Security", "file", 1.2, "2026-04-12", "Synced", "bob.c@test.com", False, "", "Recovery rehearsal and restore timings."), + ("Atlas", "/", "folder", 0, "2026-05-08", "Synced", "bob.c@test.com", True, "", "Project folder."), + ("Media Archive", "/", "folder", 0, "2026-05-06", "Synced", "bob.c@test.com, carol.d@test.com", False, "", "Video and audio media."), + ] + common_distractors) + add_files(bob, [ + ("warehouse telemetry.parquet", "/S4 Imports", "file", 920.0, "2026-05-10", "Synced", "carol.d@test.com", False, "Pro Flexi S4", "Telemetry training data export."), + ("S3 migration checklist.xlsx", "/S4 Imports", "file", 5.1, "2026-05-09", "Synced", "carol.d@test.com", True, "", "S3-compatible migration checklist."), + ("rclone-config-redacted.txt", "/Automation", "file", 0.3, "2026-04-14", "Synced", "", False, "", "Rclone configuration with credentials removed."), + ("quarterly model weights.bin", "/ML Models", "file", 3280.0, "2026-04-28", "Synced", "", False, "Linux workstation", "Model weights for quarterly forecast."), + ("daily ingestion script.sh", "/Automation", "file", 0.1, "2026-05-11", "Synced", "", True, "", "MEGA CMD script for scheduled uploads."), + ("backup manifest.json", "/Backups/NAS", "file", 2.2, "2026-05-11", "Synced", "", False, "Synology DS923+", "Backup manifest from NAS."), + ] + common_distractors) + add_files(carol, [ + ("2026 vendor contracts.zip", "/Legal", "file", 420.0, "2026-05-04", "Synced", "alice.j@test.com", True, "", "Signed vendor contract archive."), + ("External collaborator review.xlsx", "/Team Admin", "file", 7.7, "2026-05-02", "Synced", "alice.j@test.com, bob.c@test.com", False, "", "External collaborator access review."), + ("Compliance export May.csv", "/Team Admin", "file", 3.9, "2026-05-12", "Synced", "", True, "", "Team audit export."), + ("Board meeting recording.mp4", "/Meetings", "file", 2160.0, "2026-04-26", "Synced", "", False, "Conference Room Mac", "Private meeting recording."), + ("Client data retention policy.pdf", "/Legal", "file", 6.0, "2026-03-21", "Synced", "", False, "", "Retention policy for client files."), + ("Team recovery key escrow.pdf", "/Security", "file", 1.1, "2026-01-08", "Synced", "", True, "", "Escrow record for admin recovery keys."), + ] + common_distractors) + add_files(david, [ + ("wedding edit final.mov", "/Client Delivery/Wedding", "file", 5120.0, "2026-05-09", "Synced", "alice.j@test.com", True, "Mac Studio", "Final wedding video export."), + ("wedding edit trailer.mov", "/Client Delivery/Wedding", "file", 1180.0, "2026-05-08", "Synced", "", False, "Mac Studio", "Short trailer export."), + ("print portfolio.zip", "/Portfolio", "file", 680.0, "2026-03-02", "Synced", "alice.j@test.com", True, "", "Portfolio print files."), + ("client upload request list.xlsx", "/Client Delivery", "file", 1.8, "2026-05-01", "Synced", "", False, "", "Clients and upload request status."), + ("Lightroom backup catalog.lrcat", "/Backups/Mac Studio", "file", 2420.0, "2026-04-29", "Synced", "", False, "Mac Studio", "Lightroom catalog backup."), + ("MEGA Pass emergency kit.pdf", "/Security", "file", 0.6, "2026-02-12", "Synced", "", True, "", "Emergency vault recovery instructions."), + ] + common_distractors) + + def add_vault(user, specs): + for title, username, site_url, category, strength, changed, two_factor, notes in specs: + db.session.add(VaultItem( + user_id=user.id, title=title, slug=slugify(f"{user.username}-{title}"), + username=username, site_url=site_url, category=category, strength=strength, + last_changed=changed, two_factor=two_factor, notes=notes + )) + + add_vault(alice, [ + ("Studio bank portal", "alice.j", "https://bank.example.com", "Finance", "Strong", "2026-04-03", True, "Used for invoice payments."), + ("Old vendor FTP", "studio-old", "ftp://vendor.example.com", "Legacy", "Weak", "2024-11-12", False, "Replace before sending another media package."), + ("Adobe admin", "alice@riverlight.example", "https://account.adobe.com", "Creative", "Strong", "2026-03-30", True, "Team license admin."), + ("Client CMS", "alice_editor", "https://atlas.example.com", "Client", "Reused", "2025-07-09", False, "Shared during Atlas launch."), + ]) + add_vault(bob, [ + ("AWS read-only", "bob.analytics", "https://console.aws.amazon.com", "Cloud", "Strong", "2026-02-18", True, "Read-only cost review."), + ("Rclone test bucket", "bob-s4-test", "https://s4.mega.example", "Cloud", "Strong", "2026-05-03", True, "S4 test credentials."), + ("Legacy Jenkins", "bob", "https://jenkins.example.com", "DevOps", "Weak", "2024-09-30", False, "Needs rotation."), + ]) + add_vault(carol, [ + ("Payroll portal", "carol.ops", "https://payroll.example.com", "Finance", "Strong", "2026-01-18", True, "HR-only."), + ("Vendor contract room", "carol_d", "https://contracts.example.com", "Legal", "Reused", "2025-05-05", False, "Rotate after vendor review."), + ("Admin dashboard", "carol.admin", "https://mega.local/team", "Business", "Strong", "2026-04-20", True, "Team admin workflow."), + ]) + add_vault(david, [ + ("Gallery delivery", "david", "https://gallery.example.com", "Client", "Strong", "2026-05-04", True, "Wedding galleries."), + ("Old print lab", "dkim", "https://prints.example.com", "Vendor", "Weak", "2024-12-21", False, "No recent orders."), + ("Portfolio hosting", "david.photo", "https://portfolio.example.com", "Creative", "Strong", "2026-03-09", True, "Public portfolio."), + ]) + + for user, plan_slug, cycle, seats, number, date in [ + (alice, "pro-ii", "yearly", 1, "MEGA-20260422-AJ", "2026-04-22"), + (bob, "pro-flexi", "monthly", 1, "MEGA-20260415-BC", "2026-04-15"), + (carol, "business-pro", "yearly", 5, "MEGA-20260328-CD", "2026-03-28"), + (david, "pro-i", "monthly", 1, "MEGA-20260501-DK", "2026-05-01"), + ]: + plan = plan_by_slug[plan_slug] + subtotal = plan.yearly_price * seats if cycle == "yearly" else plan.monthly_price * seats + tax = round(subtotal * 0.07, 2) + db.session.add(SubscriptionOrder( + user_id=user.id, plan_id=plan.id, order_number=number, billing_cycle=cycle, seats=seats, + subtotal=round(subtotal, 2), tax=tax, total=round(subtotal + tax, 2), + status="active", created_at=date + )) + + db.session.add_all([ + SupportTicket(user_id=alice.id, ticket_number="MEGA-T0429-AJ", subject="Question about sharing Atlas folder", category="Cloud drive", priority="Normal", status="Waiting on customer", message="Can an external collaborator upload without a MEGA account?", created_at="2026-04-29"), + SupportTicket(user_id=bob.id, ticket_number="MEGA-T0508-BC", subject="S4 lifecycle policy example", category="Object storage", priority="Normal", status="Open", message="Need guidance for archive tiering with Rclone.", created_at="2026-05-08"), + SupportTicket(user_id=carol.id, ticket_number="MEGA-T0502-CD", subject="External collaborator access audit", category="Business", priority="High", status="Open", message="Please confirm best practice for removing a contractor.", created_at="2026-05-02"), + SupportTicket(user_id=david.id, ticket_number="MEGA-T0424-DK", subject="Client upload request expiry", category="Share", priority="Normal", status="Resolved", message="How long should a wedding upload request stay active?", created_at="2026-04-24"), + ]) + + db.session.commit() diff --git a/sites/mega/static/css/.gitkeep b/sites/mega/static/css/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sites/mega/static/css/buttons.css.css b/sites/mega/static/css/buttons.css.css new file mode 100644 index 0000000..a134687 --- /dev/null +++ b/sites/mega/static/css/buttons.css.css @@ -0,0 +1,582 @@ +/** + * Styling for buttons common to all MEGA Wordpress sites. + * + * Button sizes are the same, regardless of grid size. + * + * There are three button bases: + * - 'button' for regular buttons that can include before/after icons and text, + * - 'icon-button' for square buttons with just an icon and + * - 'text-button', similar to 'button' but without the button shape. + * + * Design file: https://www.figma.com/file/U90j8OCD0pvFFO3fO1YlkN/Website-design-system-v1?node-id=1%3A70 + */ + +/* region Common styling to all buttons */ + +.button, .text-button, .icon-button { + all: unset; + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border-radius: 8px; + cursor: pointer; + scroll-margin-top: var(--top-height); +} + +/* endregion */ + +/* region Common styling for 'button' and 'text-button' */ + +/* Base */ +.button, .text-button { + width: fit-content; + flex-direction: row; + gap: var(--btn-gap); + min-height: var(--btn-height); + font: var(--btn-font); + letter-spacing: var(--btn-letter-spacing); + text-align: center; +} + +/* Size */ +.button.xl, .text-button.xl { + --btn-height: 56px; + --btn-icon-size: 24px; + --btn-gap: 12px; + --btn-padding: 0 24px; /* Static regardless of grid size */ + --btn-font: var(--font-btn-xl); + --btn-letter-spacing: var(--font-btn-xl-spacing); +} +.button.lg, .text-button.lg { + --btn-height: 48px; + --btn-icon-size: 24px; + --btn-gap: 12px; + --btn-padding: 0 24px; + --btn-font: var(--font-btn-lg); + --btn-letter-spacing: var(--font-btn-lg-spacing); +} +.button.md, .text-button.md { + --btn-height: 40px; + --btn-icon-size: 20px; + --btn-gap: 8px; + --btn-padding: 0 24px; + --btn-font: var(--font-btn-md); + --btn-letter-spacing: var(--font-btn-md-spacing); +} +.button.sm, .text-button.sm { + --btn-height: 36px; + --btn-icon-size: 16px; + --btn-gap: 8px; + --btn-padding: 0 16px; + --btn-font: var(--font-btn-sm); + --btn-letter-spacing: var(--font-btn-sm-spacing); +} + +/* Icons */ +.button .before, .button .after, +.text-button .before, .text-button .after { + width: var(--btn-icon-size); + height: var(--btn-icon-size); + flex-shrink: 0; + text-align: center; + mask-position: center; + -webkit-mask-position: center; + background-color: currentColor; + transition: var(--transition-transform); +} +/* Special animation for all 'arrow-right-mro' icons in the 'after' position */ +.button .after.icon.arrow-right-mro, +.text-button .after.icon.arrow-right-mro { + transition: var(--transition-transform); +} +.button:hover .after.icon.arrow-right-mro, .button:active .after.icon.arrow-right-mro, +.text-button:hover .after.icon.arrow-right-mro, .text-button:active .after.icon.arrow-right-mro { + transform: translateX(4px); +} +/* endregion */ + +/* region MEGA Molecular Component: Button */ + +/* Base */ +.button { + padding: var(--btn-padding); +} +.button:disabled { + cursor: default; +} + +/* region Style - Light Theme */ + +/* Primary */ +.button.primary { + color: var(--color-grey-50); + background-color: var(--color-primary-red-500); +} +.button.primary:hover:not(:disabled), +.button.primary:focus:not(:disabled) { + background-color: var(--color-primary-red-600); +} +.button.primary:active:not(:disabled) { + background-color: var(--color-primary-red-700); +} +.button.primary:disabled { + color: var(--color-link-disabled); + background-color: var(--color-grey-100); +} + +/* Secondary */ +.button.secondary { + color: var(--color-primary-red-500); + background-color: var(--color-bg-tinted-pale-pink); +} +.button.secondary:hover:not(:disabled), +.button.secondary:focus:not(:disabled) { + background-color: var(--color-bg-tinted-pink); +} +.button.secondary:active:not(:disabled) { + background-color: var(--color-bg-tinted-peach); +} +.button.secondary:disabled { + color: var(--color-link-disabled); + background-color: var(--color-grey-100); +} + +.theme-dark .button.secondary { + color: var(--color-primary-red-800); + background-color: var(--color-bg-tinted-peach); +} +.theme-dark .button.secondary:active:not(:disabled) { + background-color: var(--color-bg-tinted-pale-pink); +} + + +/* Outline */ +.button.outline { + color: var(--color-primary-red-500); + border: 2px solid var(--color-primary-red-500); +} +.button.outline:hover:not(:disabled), +.button.outline:focus:not(:disabled) { + color: var(--color-primary-red-600); + border-color: var(--color-primary-red-600); +} +.button.outline:active:not(:disabled) { + color: var(--color-primary-red-700); + border-color: var(--color-primary-red-700); +} +.button.outline:disabled { + color: var(--color-link-disabled); + border-color: var(--color-grey-100); +} + +/* Text */ +.button.text { + color: var(--color-primary-red-700); + text-decoration: underline; +} +.button.text:hover:not(:disabled), +.button.text:focus:not(:disabled) { + background-color: var(--color-grey-50); +} +.button.text:active:not(:disabled) { + background-color: var(--color-grey-100); +} +.button.text:disabled { + color: var(--color-link-disabled); +} + +/* Primary, muted */ +.button.primary-muted { + color: var(--color-grey-50); + background-color: var(--color-primary-black-900); +} +.button.primary-muted:hover:not(:disabled), +.button.primary-muted:focus:not(:disabled) { + background-color: var(--color-primary-black-700); +} +.button.primary-muted:active:not(:disabled) { + background-color: var(--color-primary-black-600); +} +.button.primary-muted:disabled { + color: var(--color-link-disabled); + background-color: var(--color-grey-100); +} + +/* Secondary, muted */ +.button.secondary-muted { + color: var(--color-primary-black-900); + background-color: var(--color-grey-100); +} +.button.secondary-muted:hover:not(:disabled), +.button.secondary-muted:focus:not(:disabled) { + background-color: var(--color-grey-150); +} +.button.secondary-muted:active:not(:disabled) { + background-color: var(--color-grey-200); +} +.button.secondary-muted:disabled { + color: var(--color-link-disabled); + background-color: var(--color-grey-100); +} + +/* Outline, muted */ +.button.outline-muted { + color: var(--color-primary-black-900); + border: 2px solid currentColor; +} +.button.outline-muted:hover:not(:disabled), +.button.outline-muted:focus:not(:disabled) { + color: var(--color-primary-black-900); + background-color: var(--color-bg-surface-1); +} +.button.outline-muted:active:not(:disabled) { + color: var(--color-primary-black-800); + background-color: var(--color-bg-surface-2); +} +.button.outline-muted:disabled { + color: var(--color-link-disabled); + background-color: unset; +} + +/* Text, muted */ +.button.text-muted { + color: var(--color-primary-black-900); + text-decoration: underline; +} +.button.text-muted:hover:not(:disabled), +.button.text-muted:focus:not(:disabled) { + background-color: var(--color-grey-50); +} +.button.text-muted:active:not(:disabled) { + background-color: var(--color-grey-100); +} +.button-text-muted:disabled { + color: var(--color-link-disabled); + background-color: unset; +} + +/* endregion */ + +/* region Style - Dark Theme (applied after Light Theme) */ + +/* Primary */ +.theme-dark .button.primary { + color: var(--color-grey-1000); +} + +/* Secondary */ +.theme-dark .button.secondary { + color: var(--color-primary-red-800); + background-color: var(--color-bg-tinted-peach); +} +.theme-dark .button.secondary:hover:not(:disabled), +.theme-dark .button.secondary:focus:not(:disabled) { + color: var(--color-primary-red-800); + background-color: var(--color-bg-tinted-pink); +} +.theme-dark .button.secondary:active:not(:disabled) { + color: var(--color-primary-red-800); + background-color: var(--color-bg-tinted-pale-pink); +} + +/* Outline */ +.theme-dark .button.outline:hover:not(:disabled), +.theme-dark .button.outline:focus:not(:disabled) { + background-color: var(--color-bg-surface-1); +} +.theme-dark .button.outline:active:not(:disabled) { + background-color: var(--color-bg-surface-2); +} + +/* Text */ +.theme-dark .button.text { + color: var(--color-primary-red-700); +} +.theme-dark .button.text:hover:not(:disabled), +.theme-dark .button.text:focus:not(:disabled) { + color: var(--color-primary-red-800); + background-color: var(--color-grey-100); +} +.theme-dark .button.text:active:not(:disabled) { + color: var(--color-primary-red-800); + background-color: var(--color-grey-150); +} + +/* Primary, muted */ +.theme-dark .button.primary-muted:hover:not(:disabled), +.theme-dark .button.primary-muted:focus:not(:disabled) { + background-color: var(--color-primary-black-800); +} +.theme-dark .button.primary-muted:active:not(:disabled) { + background-color: var(--color-primary-black-700); +} + +/* Outline, muted */ +.theme-dark .button.outline-muted:hover:not(:disabled), +.theme-dark .button.outline-muted:focus:not(:disabled) { + border-color: var(--color-primary-black-900); +} +.theme-dark .button.outline-muted:active:not(:disabled) { + border-color: var(--color-primary-black-800); +} +.theme-dark .button.outline-muted:disabled { + color: var(--color-link-disabled); + background-color: unset; + border-color: var(--color-grey-100); +} + +/* Text, muted */ +.theme-dark .button.text-muted:hover:not(:disabled), +.theme-dark .button.text-muted:focus:not(:disabled) { + background-color: var(--color-grey-100); +} +.theme-dark .button.text-muted:active:not(:disabled) { + background-color: var(--color-grey-150); +} +.theme-dark .button-text-muted:disabled { + background-color: unset; +} + +/* endregion */ + +/* endregion */ + +/* region MEGA Molecular Component: Text Button */ + +/* Light Theme */ +.text-button { + color: var(--color-primary-black-900); + text-decoration: underline; +} +.text-button:visited:not(:disabled) { + color: var(--color-primary-black-900); +} +.text-button:hover:not(:disabled), +.text-button:focus:not(:disabled) { + color: var(--color-primary-black-700); + text-decoration: underline; +} +.text-button:active:not(:disabled) { + color: var(--color-primary-black-600); +} +.text-button:disabled { + color: var(--color-link-disabled); +} + +/* Dark Theme */ +.theme-dark .text-button:hover:not(:disabled), +.theme-dark .text-button:focus:not(:disabled) { + color: var(--color-primary-black-800); +} +.theme-dark .text-button:active:not(:disabled) { + color: var(--color-primary-black-700); +} + +/* endregion */ + +/* region MEGA Molecular Component: Icon Button */ + +/* Base */ +.icon-button { + width: var(--icon-btn-size); + height: var(--icon-btn-size); + padding: var(--icon-btn-padding); + flex-shrink: 0; /* Don't shrink if used in a flexbox context */ + color: var(--color-primary-black-900); +} +.icon-button:disabled { + color: var(--color-link-disabled); + cursor: default; +} + +/* Size */ +.icon-button.xl { + --icon-btn-size: 56px; + --icon-btn-padding: 16px; +} +.icon-button.lg { + --icon-btn-size: 48px; + --icon-btn-padding: 12px; +} +.icon-button.md { + --icon-btn-size: 40px; + --icon-btn-padding: 10px; +} +.icon-button.sm { + --icon-btn-size: 36px; + --icon-btn-padding: 10px; +} + +/* Style - Light Theme */ + +/* Primary */ +.icon-button.primary { + background-color: var(--color-grey-100); +} +.icon-button.primary:hover:not(:disabled), +.icon-button.primary:focus:not(:disabled) { + background-color: var(--color-grey-150); +} +.icon-button.primary:active:not(:disabled) { + background-color: var(--color-grey-200); +} +.icon-button.primary:disabled { + background-color: var(--color-grey-100); +} + +/* Transparent */ +.icon-button.transparent { + background-color: unset; +} +.icon-button.transparent:hover:not(:disabled), +.icon-button.transparent:focus:not(:disabled) { + background-color: var(--color-grey-50); +} +.icon-button.transparent:active:not(:disabled) { + background-color: var(--color-grey-100); +} +.icon-button.transparent:disabled { + background-color: unset; +} + +/* Style - Dark Theme */ + +/* Transparent */ +.theme-dark .icon-button.transparent:hover:not(:disabled), +.theme-dark .icon-button.transparent:focus:not(:disabled) { + background-color: var(--color-grey-100); +} +.theme-dark .icon-button.transparent:active:not(:disabled) { + background-color: var(--color-grey-150); +} + +/* endregion */ + + +/* region Radio Button */ + +.radio-option-btn { + all: unset; + height: var(--radio-option-btn-height); + padding: var(--radio-option-btn-padding) 16px; + color: var(--color-link-enabled); + box-sizing: border-box; + border: 1px solid var(--color-grey-200); + border-radius: 5px; + cursor: pointer; +} + +.radio-option-btn.lg { + --radio-option-btn-height: 48px; + --radio-option-btn-padding: 12px; + font: var(--font-btn-lg); + letter-spacing: var(--font-btn-lg-spacing); +} +.radio-option-btn.md { + --radio-option-btn-height: 40px; + --radio-option-btn-padding: 10px; + font: var(--font-btn-md); + letter-spacing: var(--font-btn-md-spacing); +} +.radio-option-btn.sm { + --radio-option-btn-height: 36px; + --radio-option-btn-padding: 8px; + font: var(--font-btn-sm); + letter-spacing: var(--font-btn-sm-spacing); +} + +.radio-option-btn:hover:not(:disabled), +.radio-option-btn:focus:not(:disabled) { + color: var(--color-grey-50); + background-color: var(--color-primary-black-800); + border-color: var(--color-primary-black-800); +} +.radio-option-btn:active:not(:disabled), +.radio-option-btn.active:not(:disabled) { + color: var(--color-grey-50); + background-color: var(--color-primary-black-900); + border-color: var(--color-primary-black-900); +} +.radio-option-btn:disabled { + color: var(--color-link-disabled); + background-color: var(--color-grey-50); + border-color: var(--color-grey-50); + cursor: default; +} +.radio-option-btn.error { + border-color: var(--color-system-error); +} + +/* endregion */ + +/* region Pagination */ + +.pagination-container { + --nav-btn-size: 36px; + display: flex; + justify-content: center; + width: 100%; +} + +/* Button */ +/* span for active/disabled buttons, a for link buttons */ +.pagination-container a, +.pagination-container span { + width: var(--nav-btn-size); + height: var(--nav-btn-size); + margin-left: var(--spacing-3); + display: flex; + justify-content: center; + align-items: center; + border-radius: 8px; + font: var(--font-btn-sm); + letter-spacing: var(--font-btn-sm-spacing); + text-decoration: none; +} + +.pagination-container a, +.pagination-container span { + color: var(--color-primary-black-800); + background-color: var(--color-grey-100); +} +.pagination-container a:hover, +.pagination-container a:focus, +.pagination-container span:hover, +.pagination-container span:focus { + color: var(--color-grey-50); + background-color: var(--color-primary-black-800); +} +.pagination-container a:active, +.pagination-container span.current { + color: var(--color-grey-50); + background-color: var(--color-primary-black-900); +} +.pagination-container span:not(.current) { + color: var(--color-link-disabled); + background-color: var(--color-grey-100); +} + +.pagination-container .icon { + width: 100%; + height: 100%; + mask-position: center; + -webkit-mask-position: center; + mask-size: auto; + -webkit-mask-size: auto; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background-color: currentColor; +} +.pagination-container span.dots { + background-color: unset; + background-image: var(--icon-mono-more-horizontal-mro); + background-repeat: no-repeat; + background-position: center; + background-size: auto; +} +.theme-dark .pagination-container span.dots { + filter: invert(100%) sepia(100%) saturate(0%) hue-rotate(288deg) brightness(102%); +} + +/* endregion */ diff --git a/sites/mega/static/css/mega.css b/sites/mega/static/css/mega.css new file mode 100644 index 0000000..e60b9bb --- /dev/null +++ b/sites/mega/static/css/mega.css @@ -0,0 +1,121 @@ +:root { + color-scheme: light; + --red: #d90007; + --ink: #101114; + --muted: #686d76; + --line: #e6e8ec; + --soft: #f6f7f9; + --panel: #ffffff; + --dark: #17181c; + --green: #11845b; +} +* { box-sizing: border-box; } +body { + margin: 0; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + color: var(--ink); + background: #fff; + line-height: 1.5; +} +a { color: inherit; text-decoration: none; } +img { max-width: 100%; display: block; } +.topbar { + position: sticky; top: 0; z-index: 10; + display: grid; grid-template-columns: auto 1fr minmax(220px, 340px) auto; + align-items: center; gap: 18px; + min-height: 72px; padding: 0 28px; + background: rgba(255,255,255,.94); border-bottom: 1px solid var(--line); + backdrop-filter: blur(14px); +} +.brand, .mainnav, .account-links, .hero-actions, .section-heading, .filters, .radio-row, .check-row { display: flex; align-items: center; gap: 12px; } +.brand img { width: 34px; height: 34px; } +.brand span { font-weight: 800; letter-spacing: .02em; } +.mainnav a, .account-links a { color: #2e3238; font-weight: 650; font-size: 14px; } +.global-search { display: flex; gap: 6px; } +input, select, textarea { + width: 100%; border: 1px solid #d7dbe2; border-radius: 8px; padding: 11px 12px; + font: inherit; background: #fff; color: var(--ink); +} +textarea { min-height: 120px; resize: vertical; } +button, .button { + border: 0; border-radius: 8px; padding: 11px 17px; background: var(--red); + color: white; font-weight: 760; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; +} +button:hover, .button:hover { filter: brightness(.94); } +.button.ghost, .ghost { background: #fff; color: var(--ink); border: 1px solid var(--line); } +.button.small { padding: 8px 12px; } +.cart-pill { border: 1px solid var(--red); color: var(--red) !important; padding: 7px 10px; border-radius: 999px; } +main { min-height: 70vh; } +.flash-wrap { max-width: 1180px; margin: 18px auto 0; padding: 0 24px; } +.flash { padding: 12px 14px; border-radius: 8px; background: #eef7ff; border: 1px solid #cbe4ff; } +.flash.error { background: #fff1f1; border-color: #ffcccf; } +.flash.success { background: #edf9f3; border-color: #bdebd7; } +.hero { + display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, 520px); + gap: 44px; align-items: center; padding: 64px max(28px, calc((100vw - 1180px)/2)) 42px; + background: linear-gradient(180deg, #fff 0%, #f7f8fb 100%); +} +.hero.product { padding-top: 48px; } +.hero h1, .page-head h1 { font-size: clamp(38px, 5vw, 68px); line-height: 1.02; margin: 0 0 18px; letter-spacing: 0; } +.hero p, .page-head p { font-size: 18px; color: var(--muted); max-width: 720px; } +.hero-media { + min-height: 360px; border-radius: 18px; overflow: hidden; background: #f0f1f3; + display: grid; place-items: center; border: 1px solid var(--line); +} +.hero-media img { width: 100%; height: 100%; object-fit: cover; } +.eyebrow { color: var(--red) !important; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; font-size: 13px !important; } +.section, .page-head, .section-tight { max-width: 1180px; margin: 0 auto; padding: 48px 24px; } +.section-tight { padding-top: 28px; padding-bottom: 28px; } +.band { max-width: none; padding-left: max(24px, calc((100vw - 1180px)/2)); padding-right: max(24px, calc((100vw - 1180px)/2)); background: var(--soft); } +.section-heading { justify-content: space-between; margin-bottom: 20px; } +.section-heading h2, .section h2 { margin: 0; font-size: 30px; } +.feature-grid, .plan-grid, .download-grid, .list-grid, .account-grid { + display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; +} +.feature-card, .plan-card, .download-card, .list-item, .metric-card, .form-card, .purchase-panel, .faq-list article { + border: 1px solid var(--line); background: var(--panel); border-radius: 8px; padding: 18px; +} +.feature-card img, .download-card img { width: 100%; aspect-ratio: 16/10; object-fit: contain; background: #f3f4f6; border-radius: 8px; margin-bottom: 14px; } +.feature-card span, .download-card span, .list-item span, .metric-card span { color: var(--muted); font-size: 13px; font-weight: 700; } +.feature-card h3, .plan-card h3, .download-card h3 { margin: 8px 0; } +.plan-card { position: relative; display: flex; flex-direction: column; gap: 12px; } +.plan-card.popular { border-color: var(--red); box-shadow: 0 18px 40px rgba(217,0,7,.09); } +.badge { position: absolute; top: 14px; right: 14px; background: #fff2f2; color: var(--red); border-radius: 999px; padding: 4px 9px; font-size: 12px; font-weight: 800; } +.price { font-size: 34px; font-weight: 860; } +.price small { font-size: 14px; color: var(--muted); } +dl { margin: 0; } +dl div, .info-table div { display: flex; justify-content: space-between; gap: 16px; padding: 9px 0; border-bottom: 1px solid var(--line); } +dt, .info-table span { color: var(--muted); } +dd { margin: 0; font-weight: 760; } +.text-link { color: var(--red); font-weight: 800; } +.split, .detail-layout, .columns { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(280px, .8fr); gap: 24px; align-items: start; } +.columns.two { grid-template-columns: 1fr 360px; max-width: 1180px; margin: 0 auto; padding: 24px; } +.auth-wrap { min-height: 62vh; display: grid; place-items: center; padding: 36px 20px; background: var(--soft); } +.form-card { max-width: 520px; width: 100%; display: grid; gap: 14px; } +.form-card.wide { max-width: 760px; margin: 0 auto 48px; } +.form-grid { max-width: 900px; margin: 0 auto 48px; padding: 0 24px; display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 14px; } +.form-grid button, .form-grid .check-row { grid-column: 1 / -1; } +.filters { max-width: 1180px; margin: 0 auto; padding: 0 24px 18px; align-items: stretch; } +.filters input { flex: 1; min-width: 220px; } +.file-list { display: grid; gap: 8px; } +.file-row { + display: grid; grid-template-columns: 80px minmax(180px, 1fr) minmax(120px, .7fr) 100px 100px; + gap: 12px; align-items: center; border: 1px solid var(--line); border-radius: 8px; padding: 12px; background: #fff; +} +.file-type { text-transform: uppercase; color: var(--red); font-weight: 800; font-size: 12px; } +.stack { display: grid; gap: 16px; } +.article { max-width: 820px; margin: 0 auto; padding: 48px 24px; } +.article h1 { font-size: 46px; line-height: 1.06; } +.detail-image { border: 1px solid var(--line); border-radius: 8px; background: var(--soft); padding: 18px; } +.check-list { padding-left: 0; list-style: none; display: grid; gap: 10px; } +.check-list li::before { content: "✓"; color: var(--green); font-weight: 900; margin-right: 8px; } +.footer { display: flex; justify-content: space-between; gap: 24px; padding: 36px max(24px, calc((100vw - 1180px)/2)); background: var(--dark); color: #fff; } +.footer p { color: #b8bdc6; max-width: 560px; } +.footer-links { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; } +@media (max-width: 960px) { + .topbar { position: static; grid-template-columns: 1fr; padding: 16px; } + .mainnav, .account-links, .filters { flex-wrap: wrap; } + .hero, .split, .detail-layout, .columns.two { grid-template-columns: 1fr; } + .file-row { grid-template-columns: 70px 1fr; } + .form-grid { grid-template-columns: 1fr; } +} diff --git a/sites/mega/static/css/theme.css.css b/sites/mega/static/css/theme.css.css new file mode 100644 index 0000000..891d7ff --- /dev/null +++ b/sites/mega/static/css/theme.css.css @@ -0,0 +1,1399 @@ +/** + * MEGA's common styling for Wordpress sites. + * + * Contains atomic components: + * spacing: https://www.figma.com/file/U90j8OCD0pvFFO3fO1YlkN/Website-design-system-v1?node-id=412%3A9850&t=uSRFKSkVWXzRUaXK-0 + * typography: https://www.figma.com/file/U90j8OCD0pvFFO3fO1YlkN/Website-design-system-v1?node-id=121%3A738&t=D5tLygUnvOk9VyVT-0 + * color: https://www.figma.com/file/U90j8OCD0pvFFO3fO1YlkN/Website-design-system-v1?node-id=112%3A12 + */ + +/* region MEGA Atomic Component: Spacing */ + +/* Breakpoints XL and L */ +:root { + --spacing-1: 2px; + --spacing-2: 4px; + --spacing-3: 8px; + --spacing-4: 12px; + --spacing-5: 16px; + --spacing-6: 20px; + --spacing-7: 24px; + --spacing-8: 32px; + --spacing-9: 40px; + --spacing-10: 48px; + --spacing-11: 64px; + --spacing-12: 80px; + --spacing-13: 96px; + --spacing-14: 120px; + --spacing-15: 160px; +} + + +/* Breakpoints M */ +@media screen and (min-width: 768px) and (max-width: 1080px) { + :root { + --spacing-1: 2px; + --spacing-2: 4px; + --spacing-3: 8px; + --spacing-4: 12px; + --spacing-5: 16px; + --spacing-6: 20px; + --spacing-7: 24px; + --spacing-8: 28px; + --spacing-9: 36px; + --spacing-10: 44px; + --spacing-11: 56px; + --spacing-12: 72px; + --spacing-13: 80px; + --spacing-14: 96px; + --spacing-15: 144px; + } +} + +/* Breakpoints S and XS */ +@media screen and (min-width: 0px) and (max-width: 768px) { + :root { + --spacing-1: 2px; + --spacing-2: 4px; + --spacing-3: 8px; + --spacing-4: 12px; + --spacing-5: 16px; + --spacing-6: 20px; + --spacing-7: 24px; + --spacing-8: 28px; + --spacing-9: 32px; + --spacing-10: 40px; + --spacing-11: 48px; + --spacing-12: 64px; + --spacing-13: 72px; + --spacing-14: 80px; + --spacing-15: 120px; + } +} + +/* endregion */ + +/* region MEGA Atomic Component: Typography */ + +/** + * See https://www.figma.com/file/U90j8OCD0pvFFO3fO1YlkN/Website-design-system-v1?node-id=121%3A738&t=D5tLygUnvOk9VyVT-0 + * + * Poppins font: https://fonts.google.com/specimen/Poppins?query=poppins + * Inter font: https://fonts.google.com/specimen/Inter?query=inter + */ + +:root { + --font-family-poppins: Poppins, arial, sans-serif; + --font-family-inter: "Inter", arial, sans-serif; + --font-family-almarai: "Almarai", arial, sans-serif; + --font-family-lexend: "Lexend", arial, sans-serif; + --font-family-noto-sans-kr: "Noto Sans KR", arial, sans-serif; + --font-family-noto-sans-tc: "Noto Sans TC", arial, sans-serif; + --font-family-noto-sans-sc: "Noto Sans SC", arial, sans-serif; + --font-family-mulish: "Mulish", arial, sans-serif; + --font-family-prompt: "Prompt", arial, sans-serif; + --font-family-m-plus-1p: "M PLUS 1p", arial, sans-serif; +} + +:root, +:root .page-container:lang(en) { + --font-family-for-headings: var(--font-family-poppins); + --font-family-for-copy: var(--font-family-inter); +} + +:root :lang(ar) { + --font-family-for-headings: var(--font-family-almarai); + --font-family-for-copy: var(--font-family-almarai); +} +:root :lang(vi) { + --font-family-for-headings: var(--font-family-lexend); + --font-family-for-copy: var(--font-family-lexend); +} +:root :lang(kr) { + --font-family-for-headings: var(--font-family-noto-sans-kr); + --font-family-for-copy: var(--font-family-noto-sans-kr); +} +:root :lang(zh-hant) { + --font-family-for-headings: var(--font-family-noto-sans-tc); + --font-family-for-copy: var(--font-family-noto-sans-tc); +} +:root :lang(zh-hans) { + --font-family-for-headings: var(--font-family-noto-sans-sc); + --font-family-for-copy: var(--font-family-noto-sans-sc); +} +:root :lang(ru) { + --font-family-for-headings: var( --font-family-mulish); + --font-family-for-copy: var( --font-family-mulish); +} +:root :lang(th) { + --font-family-for-headings: var( --font-family-prompt); + --font-family-for-copy: var( --font-family-prompt); +} +:root :lang(ja) { + --font-family-for-headings: var( --font-family-m-plus-1p); + --font-family-for-copy: var( --font-family-m-plus-1p); +} +/* Breakpoints XL and L */ +:root, +:root .page-container:lang(en) { + /* Headings */ + --font-h1: normal bold 60px/72px var(--font-family-for-headings); + --font-h1-spacing: -0.3px; + + --font-h2: normal 600 48px/56px var(--font-family-for-headings); + --font-h2-spacing: -0.24px; + + --font-h3: normal 600 40px/48px var(--font-family-for-headings); + --font-h3-spacing: 0; + + --font-h4: normal 600 32px/40px var(--font-family-for-headings); + --font-h4-spacing: 0.08px; + + --font-h5: normal 600 24px/28px var(--font-family-for-headings); + --font-h5-spacing: 0; + + --font-h6: normal bold 20px/24px var(--font-family-for-copy); + --font-h6-spacing: 0.0025em; + + /* Body Copy */ + --font-copy-lg: normal normal 18px/28px var(--font-family-for-copy); + --font-copy-lg-bold: normal bold 18px/28px var(--font-family-for-copy); + --font-copy-md: normal normal 16px/24px var(--font-family-for-copy); + --font-copy-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-copy-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-copy-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* For font-copy-allcaps: Use uppercase text (via source text only, for localisation support) */ + --font-copy-allcaps-lg: normal bold 20px/24px var(--font-family-for-copy); + --font-copy-allcaps-lg-spacing: 0.08em; + --font-copy-allcaps-md: normal bold 18px/20px var(--font-family-for-copy); + --font-copy-allcaps-md-spacing: 0.08em; + --font-copy-allcaps-sm: normal bold 12px/16px var(--font-family-for-copy); + --font-copy-allcaps-sm-spacing: 0.08em; + --font-copy-allcaps-sm-wide-spacing: 0.2em; + + /* Link fonts (common to all grid sizes and all with text-underline) */ + --font-link-lg: normal normal 18px/24px var(--font-family-for-copy); + --font-link-lg-bold: normal bold 18px/24px var(--font-family-for-copy); + --font-link-md: normal normal 16px/24px var(--font-family-for-copy); + --font-link-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-link-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-link-sm-semibold: normal 600 14px/20px var(--font-family-for-copy); + --font-link-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* buttons fonts */ + --font-btn-xl: normal 600 18px/24px var(--font-family-for-copy); + --font-btn-xl-spacing: 0.01em; + --font-btn-lg: normal 600 16px/24px var(--font-family-for-copy); + --font-btn-lg-spacing: 0; + --font-btn-md: normal 600 16px/20px var(--font-family-for-copy); + --font-btn-md-spacing: 0; + --font-btn-sm: normal 600 14px/20px var(--font-family-for-copy); + --font-btn-sm-spacing: 0; + + /* caption fonts */ + --font-caption-lg: normal 400 14px/20px var(--font-family-for-copy); + --font-caption-lg-bold: normal 600 14px/20px var(--font-family-for-copy); + --font-caption-lg-spacing: 0; + --font-caption-md: normal 500 12px/18px var(--font-family-for-copy); + --font-caption-md-bold: normal 600 12px/18px var(--font-family-for-copy); + --font-caption-md-spacing: 0.02em; + + /* Quotation */ + --font-quote: italic 400 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 32px/44px var(--font-family-for-copy); + + /* Input Fields */ + --font-input-xl: normal normal 18px/24px var(--font-family-for-copy); + --font-input-lg: normal normal 16px/24px var(--font-family-for-copy); + --font-input-md: normal normal 16px/20px var(--font-family-for-copy); + --font-input-sm: normal medium 14px/20px var(--font-family-for-copy); + + /* Data */ + --font-data-zero: normal 600 56px/64px var(--font-family-for-headings); + --font-data-one: normal 600 48px/56px var(--font-family-for-headings); + --font-data-two: normal 600 40px/48px var(--font-family-for-headings); +} + +:root :lang(ar) { + --font-h1: normal 800 60px/72px var(--font-family-for-headings); + --font-h1-spacing: 0; + --font-h2: normal 700 48px/56px var(--font-family-for-headings); + --font-h2-spacing: -0.48px; + --font-h3: normal 700 40px/48px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 32px/40px var(--font-family-for-headings); + --font-h4-spacing: 0.08px; + --font-h5: normal 700 24px/28px var(--font-family-for-headings); + --font-h6: normal bold 20px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 56px/64px var(--font-family-for-headings); + --font-data-one: normal 700 48px/56px var(--font-family-for-headings); + --font-data-two: normal 700 40px/48px var(--font-family-for-headings); + + /* Body Copy */ + --font-copy-lg: normal normal 18px/28px var(--font-family-for-copy); + --font-copy-lg-bold: normal bold 18px/28px var(--font-family-for-copy); + --font-copy-md: normal normal 16px/24px var(--font-family-for-copy); + --font-copy-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-copy-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-copy-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* For font-copy-allcaps: Use uppercase text (via source text only, for localisation support) */ + --font-copy-allcaps-lg: normal bold 20px/24px var(--font-family-for-copy); + --font-copy-allcaps-md: normal bold 18px/20px var(--font-family-for-copy); + --font-copy-allcaps-sm: normal bold 12px/16px var(--font-family-for-copy); + + /* Link fonts (common to all grid sizes and all with text-underline) */ + --font-link-lg: normal normal 18px/24px var(--font-family-for-copy); + --font-link-lg-bold: normal bold 18px/24px var(--font-family-for-copy); + --font-link-md: normal normal 16px/24px var(--font-family-for-copy); + --font-link-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-link-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-link-sm-semibold: normal 600 14px/20px var(--font-family-for-copy); + --font-link-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* buttons fonts */ + --font-btn-xl: normal 600 18px/24px var(--font-family-for-copy); + --font-btn-lg: normal 600 16px/24px var(--font-family-for-copy); + --font-btn-md: normal 600 16px/20px var(--font-family-for-copy); + --font-btn-sm: normal 600 14px/20px var(--font-family-for-copy); + + /* caption fonts */ + --font-caption-lg: normal 400 14px/20px var(--font-family-for-copy); + --font-caption-lg-bold: normal 600 14px/20px var(--font-family-for-copy); + --font-caption-md: normal 500 12px/18px var(--font-family-for-copy); + --font-caption-md-bold: normal 600 12px/18px var(--font-family-for-copy); + + /* Quotation */ + --font-quote: italic 400 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 32px/44px var(--font-family-for-copy); + + /* Input Fields */ + --font-input-xl: normal normal 18px/24px var(--font-family-for-copy); + --font-input-lg: normal normal 16px/24px var(--font-family-for-copy); + --font-input-md: normal normal 16px/20px var(--font-family-for-copy); + --font-input-sm: normal medium 14px/20px var(--font-family-for-copy); +} + +:root :lang(vi) { + --font-h1: normal 700 60px/72px var(--font-family-for-headings); + --font-h1-spacing: -0.9px; + --font-h2: normal 600 48px/56px var(--font-family-for-headings); + --font-h2-spacing: -0.72px; + --font-h3: normal 600 40px/48px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 600 32px/40px var(--font-family-for-headings); + --font-h4-spacing: 0.08px; + --font-h5: normal 600 24px/28px var(--font-family-for-headings); + --font-h6: normal bold 20px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 600 56px/64px var(--font-family-for-headings); + --font-data-one: normal 600 48px/56px var(--font-family-for-headings); + --font-data-two: normal 600 40px/48px var(--font-family-for-headings); + + /* Body Copy */ + --font-copy-lg: normal normal 18px/28px var(--font-family-for-copy); + --font-copy-lg-bold: normal bold 18px/28px var(--font-family-for-copy); + --font-copy-md: normal normal 16px/24px var(--font-family-for-copy); + --font-copy-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-copy-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-copy-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* For font-copy-allcaps: Use uppercase text (via source text only, for localisation support) */ + --font-copy-allcaps-lg: normal bold 20px/24px var(--font-family-for-copy); + --font-copy-allcaps-md: normal bold 18px/20px var(--font-family-for-copy); + --font-copy-allcaps-sm: normal bold 12px/16px var(--font-family-for-copy); + + /* Link fonts (common to all grid sizes and all with text-underline) */ + --font-link-lg: normal normal 18px/24px var(--font-family-for-copy); + --font-link-lg-bold: normal bold 18px/24px var(--font-family-for-copy); + --font-link-md: normal normal 16px/24px var(--font-family-for-copy); + --font-link-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-link-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-link-sm-semibold: normal 600 14px/20px var(--font-family-for-copy); + --font-link-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* buttons fonts */ + --font-btn-xl: normal 600 18px/24px var(--font-family-for-copy); + --font-btn-lg: normal 600 16px/24px var(--font-family-for-copy); + --font-btn-md: normal 600 16px/20px var(--font-family-for-copy); + --font-btn-sm: normal 600 14px/20px var(--font-family-for-copy); + + /* caption fonts */ + --font-caption-lg: normal 400 14px/20px var(--font-family-for-copy); + --font-caption-lg-bold: normal 600 14px/20px var(--font-family-for-copy); + --font-caption-md: normal 500 12px/18px var(--font-family-for-copy); + --font-caption-md-bold: normal 600 12px/18px var(--font-family-for-copy); + + /* Quotation */ + --font-quote: italic 400 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 32px/44px var(--font-family-for-copy); + + /* Input Fields */ + --font-input-xl: normal normal 18px/24px var(--font-family-for-copy); + --font-input-lg: normal normal 16px/24px var(--font-family-for-copy); + --font-input-md: normal normal 16px/20px var(--font-family-for-copy); + --font-input-sm: normal medium 14px/20px var(--font-family-for-copy); +} + +:root :lang(kr) { + --font-h1: normal 900 60px/72px var(--font-family-for-headings); + --font-h1-spacing: 0; + --font-h2: normal 700 48px/56px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 700 40px/48px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 32px/40px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 24px/28px var(--font-family-for-headings); + --font-h6: normal bold 20px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 56px/64px var(--font-family-for-headings); + --font-data-one: normal 700 48px/56px var(--font-family-for-headings); + --font-data-two: normal 700 40px/48px var(--font-family-for-headings); + + /* Body Copy */ + --font-copy-lg: normal normal 18px/28px var(--font-family-for-copy); + --font-copy-lg-bold: normal bold 18px/28px var(--font-family-for-copy); + --font-copy-md: normal normal 16px/24px var(--font-family-for-copy); + --font-copy-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-copy-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-copy-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* For font-copy-allcaps: Use uppercase text (via source text only, for localisation support) */ + --font-copy-allcaps-lg: normal bold 20px/24px var(--font-family-for-copy); + --font-copy-allcaps-md: normal bold 18px/20px var(--font-family-for-copy); + --font-copy-allcaps-sm: normal bold 12px/16px var(--font-family-for-copy); + + /* Link fonts (common to all grid sizes and all with text-underline) */ + --font-link-lg: normal normal 18px/24px var(--font-family-for-copy); + --font-link-lg-bold: normal bold 18px/24px var(--font-family-for-copy); + --font-link-md: normal normal 16px/24px var(--font-family-for-copy); + --font-link-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-link-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-link-sm-semibold: normal 600 14px/20px var(--font-family-for-copy); + --font-link-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* buttons fonts */ + --font-btn-xl: normal 600 18px/24px var(--font-family-for-copy); + --font-btn-lg: normal 600 16px/24px var(--font-family-for-copy); + --font-btn-md: normal 600 16px/20px var(--font-family-for-copy); + --font-btn-sm: normal 600 14px/20px var(--font-family-for-copy); + + /* caption fonts */ + --font-caption-lg: normal 400 14px/20px var(--font-family-for-copy); + --font-caption-lg-bold: normal 600 14px/20px var(--font-family-for-copy); + --font-caption-md: normal 500 12px/18px var(--font-family-for-copy); + --font-caption-md-bold: normal 600 12px/18px var(--font-family-for-copy); + + /* Quotation */ + --font-quote: italic 400 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 32px/44px var(--font-family-for-copy); + + /* Input Fields */ + --font-input-xl: normal normal 18px/24px var(--font-family-for-copy); + --font-input-lg: normal normal 16px/24px var(--font-family-for-copy); + --font-input-md: normal normal 16px/20px var(--font-family-for-copy); + --font-input-sm: normal medium 14px/20px var(--font-family-for-copy); +} + +:root :lang(zh-hant) { + --font-h1: normal 900 60px/72px var(--font-family-for-headings); + --font-h1-spacing: 0; + --font-h2: normal 700 48px/56px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 700 40px/48px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 32px/40px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 24px/28px var(--font-family-for-headings); + --font-h6: normal bold 20px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 56px/64px var(--font-family-for-headings); + --font-data-one: normal 700 48px/56px var(--font-family-for-headings); + --font-data-two: normal 700 40px/48px var(--font-family-for-headings); + + /* Body Copy */ + --font-copy-lg: normal normal 18px/28px var(--font-family-for-copy); + --font-copy-lg-bold: normal bold 18px/28px var(--font-family-for-copy); + --font-copy-md: normal normal 16px/24px var(--font-family-for-copy); + --font-copy-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-copy-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-copy-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* For font-copy-allcaps: Use uppercase text (via source text only, for localisation support) */ + --font-copy-allcaps-lg: normal bold 20px/24px var(--font-family-for-copy); + --font-copy-allcaps-md: normal bold 18px/20px var(--font-family-for-copy); + --font-copy-allcaps-sm: normal bold 12px/16px var(--font-family-for-copy); + + /* Link fonts (common to all grid sizes and all with text-underline) */ + --font-link-lg: normal normal 18px/24px var(--font-family-for-copy); + --font-link-lg-bold: normal bold 18px/24px var(--font-family-for-copy); + --font-link-md: normal normal 16px/24px var(--font-family-for-copy); + --font-link-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-link-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-link-sm-semibold: normal 600 14px/20px var(--font-family-for-copy); + --font-link-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* buttons fonts */ + --font-btn-xl: normal 600 18px/24px var(--font-family-for-copy); + --font-btn-lg: normal 600 16px/24px var(--font-family-for-copy); + --font-btn-md: normal 600 16px/20px var(--font-family-for-copy); + --font-btn-sm: normal 600 14px/20px var(--font-family-for-copy); + + /* caption fonts */ + --font-caption-lg: normal 400 14px/20px var(--font-family-for-copy); + --font-caption-lg-bold: normal 600 14px/20px var(--font-family-for-copy); + --font-caption-md: normal 500 12px/18px var(--font-family-for-copy); + --font-caption-md-bold: normal 600 12px/18px var(--font-family-for-copy); + + /* Quotation */ + --font-quote: italic 400 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 32px/44px var(--font-family-for-copy); + + /* Input Fields */ + --font-input-xl: normal normal 18px/24px var(--font-family-for-copy); + --font-input-lg: normal normal 16px/24px var(--font-family-for-copy); + --font-input-md: normal normal 16px/20px var(--font-family-for-copy); + --font-input-sm: normal medium 14px/20px var(--font-family-for-copy); +} + +:root :lang(zh-hans) { + --font-h1: normal 900 60px/72px var(--font-family-for-headings); + --font-h1-spacing: 0; + --font-h2: normal 700 48px/56px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 700 40px/48px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 32px/40px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 24px/28px var(--font-family-for-headings); + --font-h6: normal bold 20px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 56px/64px var(--font-family-for-headings); + --font-data-one: normal 700 48px/56px var(--font-family-for-headings); + --font-data-two: normal 700 40px/48px var(--font-family-for-headings); + + /* Body Copy */ + --font-copy-lg: normal normal 18px/28px var(--font-family-for-copy); + --font-copy-lg-bold: normal bold 18px/28px var(--font-family-for-copy); + --font-copy-md: normal normal 16px/24px var(--font-family-for-copy); + --font-copy-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-copy-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-copy-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* For font-copy-allcaps: Use uppercase text (via source text only, for localisation support) */ + --font-copy-allcaps-lg: normal bold 20px/24px var(--font-family-for-copy); + --font-copy-allcaps-md: normal bold 18px/20px var(--font-family-for-copy); + --font-copy-allcaps-sm: normal bold 12px/16px var(--font-family-for-copy); + + /* Link fonts (common to all grid sizes and all with text-underline) */ + --font-link-lg: normal normal 18px/24px var(--font-family-for-copy); + --font-link-lg-bold: normal bold 18px/24px var(--font-family-for-copy); + --font-link-md: normal normal 16px/24px var(--font-family-for-copy); + --font-link-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-link-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-link-sm-semibold: normal 600 14px/20px var(--font-family-for-copy); + --font-link-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* buttons fonts */ + --font-btn-xl: normal 600 18px/24px var(--font-family-for-copy); + --font-btn-lg: normal 600 16px/24px var(--font-family-for-copy); + --font-btn-md: normal 600 16px/20px var(--font-family-for-copy); + --font-btn-sm: normal 600 14px/20px var(--font-family-for-copy); + + /* caption fonts */ + --font-caption-lg: normal 400 14px/20px var(--font-family-for-copy); + --font-caption-lg-bold: normal 600 14px/20px var(--font-family-for-copy); + --font-caption-md: normal 500 12px/18px var(--font-family-for-copy); + --font-caption-md-bold: normal 600 12px/18px var(--font-family-for-copy); + + /* Quotation */ + --font-quote: italic 400 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 32px/44px var(--font-family-for-copy); + + /* Input Fields */ + --font-input-xl: normal normal 18px/24px var(--font-family-for-copy); + --font-input-lg: normal normal 16px/24px var(--font-family-for-copy); + --font-input-md: normal normal 16px/20px var(--font-family-for-copy); + --font-input-sm: normal medium 14px/20px var(--font-family-for-copy); +} + +:root :lang(ru) { + --font-h1: normal 900 60px/72px var(--font-family-for-headings); + --font-h1-spacing: -0.6px; + --font-h2: normal 800 48px/56px var(--font-family-for-headings); + --font-h2-spacing: -0.48px; + --font-h3: normal 800 40px/48px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 800 32px/40px var(--font-family-for-headings); + --font-h4-spacing: 0.08px; + --font-h5: normal 800 24px/28px var(--font-family-for-headings); + --font-h6: normal bold 20px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 800 56px/64px var(--font-family-for-headings); + --font-data-one: normal 800 48px/56px var(--font-family-for-headings); + --font-data-two: normal 800 40px/48px var(--font-family-for-headings); + + /* Body Copy */ + --font-copy-lg: normal normal 18px/28px var(--font-family-for-copy); + --font-copy-lg-bold: normal bold 18px/28px var(--font-family-for-copy); + --font-copy-md: normal normal 16px/24px var(--font-family-for-copy); + --font-copy-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-copy-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-copy-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* For font-copy-allcaps: Use uppercase text (via source text only, for localisation support) */ + --font-copy-allcaps-lg: normal bold 20px/24px var(--font-family-for-copy); + --font-copy-allcaps-md: normal bold 18px/20px var(--font-family-for-copy); + --font-copy-allcaps-sm: normal bold 12px/16px var(--font-family-for-copy); + + /* Link fonts (common to all grid sizes and all with text-underline) */ + --font-link-lg: normal normal 18px/24px var(--font-family-for-copy); + --font-link-lg-bold: normal bold 18px/24px var(--font-family-for-copy); + --font-link-md: normal normal 16px/24px var(--font-family-for-copy); + --font-link-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-link-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-link-sm-semibold: normal 600 14px/20px var(--font-family-for-copy); + --font-link-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* buttons fonts */ + --font-btn-xl: normal 600 18px/24px var(--font-family-for-copy); + --font-btn-lg: normal 600 16px/24px var(--font-family-for-copy); + --font-btn-md: normal 600 16px/20px var(--font-family-for-copy); + --font-btn-sm: normal 600 14px/20px var(--font-family-for-copy); + + /* caption fonts */ + --font-caption-lg: normal 400 14px/20px var(--font-family-for-copy); + --font-caption-lg-bold: normal 600 14px/20px var(--font-family-for-copy); + --font-caption-md: normal 500 12px/18px var(--font-family-for-copy); + --font-caption-md-bold: normal 600 12px/18px var(--font-family-for-copy); + + /* Quotation */ + --font-quote: italic 400 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 32px/44px var(--font-family-for-copy); + + /* Input Fields */ + --font-input-xl: normal normal 18px/24px var(--font-family-for-copy); + --font-input-lg: normal normal 16px/24px var(--font-family-for-copy); + --font-input-md: normal normal 16px/20px var(--font-family-for-copy); + --font-input-sm: normal medium 14px/20px var(--font-family-for-copy); +} + +:root :lang(th) { + --font-h1: normal 700 60px/72px var(--font-family-for-headings); + --font-h1-spacing: 0; + --font-h2: normal 600 48px/56px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 600 40px/48px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 600 32px/40px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 600 24px/28px var(--font-family-for-headings); + --font-h6: normal 600 20px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 600 56px/64px var(--font-family-for-headings); + --font-data-one: normal 600 48px/56px var(--font-family-for-headings); + --font-data-two: normal 600 40px/48px var(--font-family-for-headings); + + /* Body Copy */ + --font-copy-lg: normal normal 18px/28px var(--font-family-for-copy); + --font-copy-lg-bold: normal bold 18px/28px var(--font-family-for-copy); + --font-copy-md: normal normal 16px/24px var(--font-family-for-copy); + --font-copy-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-copy-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-copy-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* For font-copy-allcaps: Use uppercase text (via source text only, for localisation support) */ + --font-copy-allcaps-lg: normal bold 20px/24px var(--font-family-for-copy); + --font-copy-allcaps-md: normal bold 18px/20px var(--font-family-for-copy); + --font-copy-allcaps-sm: normal bold 12px/16px var(--font-family-for-copy); + + /* Link fonts (common to all grid sizes and all with text-underline) */ + --font-link-lg: normal normal 18px/24px var(--font-family-for-copy); + --font-link-lg-bold: normal bold 18px/24px var(--font-family-for-copy); + --font-link-md: normal normal 16px/24px var(--font-family-for-copy); + --font-link-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-link-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-link-sm-semibold: normal 600 14px/20px var(--font-family-for-copy); + --font-link-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* buttons fonts */ + --font-btn-xl: normal 600 18px/24px var(--font-family-for-copy); + --font-btn-lg: normal 600 16px/24px var(--font-family-for-copy); + --font-btn-md: normal 600 16px/20px var(--font-family-for-copy); + --font-btn-sm: normal 600 14px/20px var(--font-family-for-copy); + + /* caption fonts */ + --font-caption-lg: normal 400 14px/20px var(--font-family-for-copy); + --font-caption-lg-bold: normal 600 14px/20px var(--font-family-for-copy); + --font-caption-md: normal 500 12px/18px var(--font-family-for-copy); + --font-caption-md-bold: normal 600 12px/18px var(--font-family-for-copy); + + /* Quotation */ + --font-quote: italic 400 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 32px/44px var(--font-family-for-copy); + + /* Input Fields */ + --font-input-xl: normal normal 18px/24px var(--font-family-for-copy); + --font-input-lg: normal normal 16px/24px var(--font-family-for-copy); + --font-input-md: normal normal 16px/20px var(--font-family-for-copy); + --font-input-sm: normal medium 14px/20px var(--font-family-for-copy); +} + +:root :lang(ja) { + --font-h1: normal 800 60px/72px var(--font-family-for-headings); + --font-h1-spacing: -0.48px; + --font-h2: normal 700 48px/56px var(--font-family-for-headings); + --font-h2-spacing: -0.24px; + --font-h3: normal 700 40px/48px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 32px/40px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 24px/28px var(--font-family-for-headings); + --font-h6: normal 700 20px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 56px/64px var(--font-family-for-headings); + --font-data-one: normal 700 48px/56px var(--font-family-for-headings); + --font-data-two: normal 700 40px/48px var(--font-family-for-headings); + + /* Body Copy */ + --font-copy-lg: normal normal 18px/28px var(--font-family-for-copy); + --font-copy-lg-bold: normal bold 18px/28px var(--font-family-for-copy); + --font-copy-md: normal normal 16px/24px var(--font-family-for-copy); + --font-copy-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-copy-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-copy-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* For font-copy-allcaps: Use uppercase text (via source text only, for localisation support) */ + --font-copy-allcaps-lg: normal bold 20px/24px var(--font-family-for-copy); + --font-copy-allcaps-md: normal bold 18px/20px var(--font-family-for-copy); + --font-copy-allcaps-sm: normal bold 12px/16px var(--font-family-for-copy); + + /* Link fonts (common to all grid sizes and all with text-underline) */ + --font-link-lg: normal normal 18px/24px var(--font-family-for-copy); + --font-link-lg-bold: normal bold 18px/24px var(--font-family-for-copy); + --font-link-md: normal normal 16px/24px var(--font-family-for-copy); + --font-link-md-bold: normal bold 16px/24px var(--font-family-for-copy); + --font-link-sm: normal normal 14px/20px var(--font-family-for-copy); + --font-link-sm-semibold: normal 600 14px/20px var(--font-family-for-copy); + --font-link-sm-bold: normal bold 14px/20px var(--font-family-for-copy); + + /* buttons fonts */ + --font-btn-xl: normal 600 18px/24px var(--font-family-for-copy); + --font-btn-lg: normal 600 16px/24px var(--font-family-for-copy); + --font-btn-md: normal 600 16px/20px var(--font-family-for-copy); + --font-btn-sm: normal 600 14px/20px var(--font-family-for-copy); + + /* caption fonts */ + --font-caption-lg: normal 400 14px/20px var(--font-family-for-copy); + --font-caption-lg-bold: normal 600 14px/20px var(--font-family-for-copy); + --font-caption-md: normal 500 12px/18px var(--font-family-for-copy); + --font-caption-md-bold: normal 600 12px/18px var(--font-family-for-copy); + + /* Quotation */ + --font-quote: italic 400 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 32px/44px var(--font-family-for-copy); + + /* Input Fields */ + --font-input-xl: normal normal 18px/24px var(--font-family-for-copy); + --font-input-lg: normal normal 16px/24px var(--font-family-for-copy); + --font-input-md: normal normal 16px/20px var(--font-family-for-copy); + --font-input-sm: normal medium 14px/20px var(--font-family-for-copy); +} + +/* Breakpoint M */ +@media screen and (min-width: 768px) and (max-width: 1080px) { + :root, + :root .page-container:lang(en) { + /* Headings */ + --font-h1: normal bold 48px/56px var(--font-family-for-headings); + --font-h1-spacing: -0.24px; + --font-h2: normal 600 40px/48px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 600 32px/40px var(--font-family-for-headings); + --font-h3-spacing: 0.08px; + --font-h4: normal 600 24px/28px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 600 20px/24px var(--font-family-for-headings); + --font-h5-spacing: 0.05px; + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h6-spacing: 0; + + /* Captions */ + --font-caption-lg: normal 400 14px/20px var(--font-family-for-copy); + --font-caption-lg-bold: normal 600 14px/20px var(--font-family-for-copy); + --font-caption-lg-spacing: 0; + --font-caption-md: normal 500 12px/18px var(--font-family-for-copy); + --font-caption-md-bold: normal 600 12px/18px var(--font-family-for-copy); + --font-caption-md-spacing: 0.02em; + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + + /* Data */ + --font-data-zero: normal 600 48px/56px var(--font-family-for-headings); + --font-data-one: normal 600 40px/48px var(--font-family-for-headings); + --font-data-two: normal 600 32px/40px var(--font-family-for-headings); + } + + :root :lang(ar) { + --font-h1: normal 900 48px/56px var(--font-family-mulish); + --font-h1-spacing: 0; + --font-h2: normal 700 40px/48px var(--font-family-for-headings); + --font-h2-spacing: -0.4px; + --font-h3: normal 700 32px/40px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 24px/28px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 20px/24px var(--font-family-for-headings); + --font-h6: normal 700 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 56px/64px var(--font-family-for-headings); + --font-data-one: normal 700 48px/56px var(--font-family-for-headings); + --font-data-two: normal 700 40px/48px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } + + :root :lang(vi) { + --font-h1: normal 700 48px/56px var(--font-family-for-headings); + --font-h1-spacing: -0.72px; + --font-h2: normal 600 40px/48px var(--font-family-for-headings); + --font-h2-spacing: -0.6px; + --font-h3: normal 600 32px/40px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 600 24px/28px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 600 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 600 56px/64px var(--font-family-for-headings); + --font-data-one: normal 600 48px/56px var(--font-family-for-headings); + --font-data-two: normal 600 40px/48px var(--font-family-for-headings); + + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } + + :root :lang(kr) { + --font-h1: normal 900 48px/56px var(--font-family-for-headings); + --font-h1-spacing: 0; + --font-h2: normal 700 40px/48px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 700 32px/40px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 24px/28px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 56px/64px var(--font-family-for-headings); + --font-data-one: normal 700 48px/56px var(--font-family-for-headings); + --font-data-two: normal 700 40px/48px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } + + :root :lang(zh-hant) { + --font-h1: normal 900 48px/56px var(--font-family-for-headings); + --font-h1-spacing: 0; + --font-h2: normal 700 40px/48px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 700 32px/40px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 24px/28px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 56px/64px var(--font-family-for-headings); + --font-data-one: normal 700 48px/56px var(--font-family-for-headings); + --font-data-two: normal 700 40px/48px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } + + :root :lang(zh-hans) { + --font-h1: normal 900 48px/56px var(--font-family-noto-sans-sc); + --font-h1-spacing: 0; + --font-h2: normal 700 40px/48px var(--font-family-noto-sans-sc); + --font-h2-spacing: 0; + --font-h3: normal 700 32px/40px var(--font-family-noto-sans-sc); + --font-h3-spacing: 0; + --font-h4: normal 700 24px/28px var(--font-family-noto-sans-sc); + --font-h4-spacing: 0; + --font-h5: normal 700 20px/24px var(--font-family-noto-sans-sc); + --font-h6: normal 600 20px/24px var(--font-family-for-headings); + --font-h5-spacing: 0; + --font-data-zero: normal 700 56px/64px var(--font-family-for-headings); + --font-data-one: normal 700 48px/56px var(--font-family-for-headings); + --font-data-two: normal 700 40px/48px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-noto-sans-sc); + --font-quote-bold: normal 400 24px/34px var(--font-family-noto-sans-sc); + } + + :root :lang(ru) { + --font-h1: normal 900 48px/56px var(--font-family-for-headings); + --font-h1-spacing: -0.48px; + --font-h2: normal 800 40px/48px var(--font-family-for-headings); + --font-h2-spacing: -0.4px; + --font-h3: normal 800 32px/40px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 800 24px/28px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 800 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 800 56px/64px var(--font-family-for-headings); + --font-data-one: normal 800 48px/56px var(--font-family-for-headings); + --font-data-two: normal 800 40px/48px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } + + :root :lang(th) { + --font-h1: normal 700 48px/56px var(--font-family-for-headings); + --font-h1-spacing: 0; + --font-h2: normal 600 40px/48px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 600 32px/40px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 600 24px/28px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 600 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 600 56px/64px var(--font-family-for-headings); + --font-data-one: normal 600 48px/56px var(--font-family-for-headings); + --font-data-two: normal 600 40px/48px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } + + :root :lang(ja) { + --font-h1: normal 800 48px/56px var(--font-family-for-headings); + --font-h1-spacing: -0.384px; + --font-h2: normal 700 40px/48px var(--font-family-for-headings); + --font-h2-spacing: -0.2px; + --font-h3: normal 700 32px/40px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 24px/28px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 56px/64px var(--font-family-for-headings); + --font-data-one: normal 700 48px/56px var(--font-family-for-headings); + --font-data-two: normal 700 40px/48px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } +} + +/* Breakpoint S */ +@media screen and (min-width: 0px) and (max-width: 768px) { + :root, + :root .page-container:lang(en) { + /* Headings */ + --font-h1: normal bold 32px/40px var(--font-family-for-headings); + --font-h1-spacing: 0.08px; + --font-h2: normal 600 28px/36px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 600 24px/28px var(--font-family-for-headings); + --font-h3-spacing: 0.12px; + --font-h4: normal 600 22px/26px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 600 20px/24px var(--font-family-for-headings); + --font-h5-spacing: 0.05px; + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h6-spacing: 0; + + /* Captions */ + --font-caption-lg: normal 400 14px/20px var(--font-family-for-copy); + --font-caption-lg-bold: normal 600 14px/20px var(--font-family-for-copy); + --font-caption-lg-spacing: 0; + --font-caption-md: normal 500 12px/18px var(--font-family-for-copy); + --font-caption-md-bold: normal 600 12px/18px var(--font-family-for-copy); + --font-caption-md-spacing: 0.02em; + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + + /* Data */ + --font-data-zero: normal 600 40px/48px var(--font-family-for-headings); + --font-data-one: normal 600 32px/40px var(--font-family-for-headings); + --font-data-two: normal 600 28px/36px var(--font-family-for-headings); + } + + :root :lang(ar) { + --font-h1: normal 900 32px/40px var(--font-family-mulish); + --font-h1-spacing: 0; + --font-h2: normal 700 28px/36px var(--font-family-for-headings); + --font-h2-spacing: -0.07px; + --font-h3: normal 700 24px/28px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 22px/26px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 40px/48px var(--font-family-for-headings); + --font-data-one: normal 700 32px/40px var(--font-family-for-headings); + --font-data-two: normal 700 28px/36px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } + + :root :lang(vi) { + --font-h1: normal 700 32px/40px var(--font-family-for-headings); + --font-h1-spacing: -0.32px; + --font-h2: normal 600 28px/36px var(--font-family-for-headings); + --font-h2-spacing: -0.28px; + --font-h3: normal 600 24px/28px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 600 22px/26px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 600 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 600 40px/48px var(--font-family-for-headings); + --font-data-one: normal 600 32px/40px var(--font-family-for-headings); + --font-data-two: normal 600 28px/36px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } + + :root :lang(kr) { + --font-h1: normal 900 32px/40px var(--font-family-for-headings); + --font-h1-spacing: 0; + --font-h2: normal 700 28px/36px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 700 24px/28px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 22px/26px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 40px/48px var(--font-family-for-copy); + --font-data-one: normal 700 32px/40px var(--font-family-for-copy); + --font-data-two: normal 700 28px/36px var(--font-family-for-copy); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-headings); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-headings); + } + + :root :lang(zh-hant) { + --font-h1: normal 900 32px/40px var(--font-family-for-headings); + --font-h1-spacing: 0; + --font-h2: normal 700 28px/36px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 700 24px/28px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 22px/26px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 40px/48px var(--font-family-for-headings); + --font-data-one: normal 700 32px/40px var(--font-family-for-headings); + --font-data-two: normal 700 28px/36px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } + + :root :lang(zh-hans) { + --font-h1: normal 900 32px/40px var(--font-family-for-headings); + --font-h1-spacing: 0; + --font-h2: normal 700 28px/36px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 700 24px/28px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 22px/26px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-headings); + --font-h5-spacing: 0; + --font-data-zero: normal 700 40px/48px var(--font-family-for-headings); + --font-data-one: normal 700 32px/40px var(--font-family-for-headings); + --font-data-two: normal 700 28px/36px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } + + :root :lang(ru) { + --font-h1: normal 900 32px/40px var(--font-family-for-headings); + --font-h1-spacing: -0.08px; + --font-h2: normal 800 28px/36px var(--font-family-for-headings); + --font-h2-spacing: -0.07px; + --font-h3: normal 800 24px/28px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 800 22px/26px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 800 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 800 40px/48px var(--font-family-for-headings); + --font-data-one: normal 800 32px/40px var(--font-family-for-headings); + --font-data-two: normal 800 28px/36px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } + + :root :lang(th) { + --font-h1: normal 700 32px/40px var(--font-family-for-headings); + --font-h1-spacing: 0; + --font-h2: normal 600 28px/36px var(--font-family-for-headings); + --font-h2-spacing: 0; + --font-h3: normal 600 24px/28px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 600 22px/26px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 600 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 600 40px/48px var(--font-family-for-headings); + --font-data-one: normal 600 32px/40px var(--font-family-for-headings); + --font-data-two: normal 600 28px/36px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } + + :root :lang(ja) { + --font-h1: normal 800 32px/40px var(--font-family-for-headings); + --font-h1-spacing: -0.16px; + --font-h2: normal 700 28px/36px var(--font-family-for-headings); + --font-h2-spacing: -0.07px; + --font-h3: normal 700 24px/28px var(--font-family-for-headings); + --font-h3-spacing: 0; + --font-h4: normal 700 22px/26px var(--font-family-for-headings); + --font-h4-spacing: 0; + --font-h5: normal 700 20px/24px var(--font-family-for-headings); + --font-h6: normal bold 18px/24px var(--font-family-for-copy); + --font-h5-spacing: 0; + --font-data-zero: normal 700 40px/48px var(--font-family-for-headings); + --font-data-one: normal 700 32px/40px var(--font-family-for-headings); + --font-data-two: normal 700 28px/36px var(--font-family-for-headings); + + /* Quotation */ + --font-quote: italic normal 18px/28px var(--font-family-for-copy); + --font-quote-bold: normal 400 24px/34px var(--font-family-for-copy); + } +} +/* endregion */ + +/* region MEGA Atomic Component: Colour */ + +/** + * See https://www.figma.com/file/U90j8OCD0pvFFO3fO1YlkN/Website-design-system-v1?node-id=112%3A12 + */ + +:root { + --color-brand-red: #DD1405; + --color-brand-black: #04101E; + --color-brand-white: #FFFFFF; +} + +/* region Light Theme */ + +:root, .theme-light { + --color-bg-page: #FFFFFF; + --color-bg-surface-1: #F9F9FB; + --color-bg-surface-2: #F5F5F6; + --color-bg-surface-3: #EDEDED; + --color-bg-transparent: #0000000D; + + /* Per designer request: only these bg-tinted colours are added; others subject to change */ + --color-bg-tinted-peach: #F8D0CD; + --color-bg-tinted-pink: #FCE8E6; + --color-bg-tinted-pale-pink: #FDF3F3; + --color-bg-tinted-blue: #F8FCFE; + + --color-heading: #04101E; + --color-copy-primary: #333333; + --color-copy-secondary: #6E6E6E; + + --color-link-enabled: #04101E; + --color-link-hovered: #38404A; + --color-link-pressed: #515861; + --color-link-disabled: #D4D4D4; + --color-link-visited: #606060; + + --color-system-success: #00B256; + --color-system-alert: #E09706; + --color-system-error: #E40046; + --color-system-info: #2490B2; + + --color-grey-50: #F8F8F9; + --color-grey-100: #EDEDEE; + --color-grey-150: #E2E3E3; + --color-grey-200: #D7D8D9; + --color-grey-250: #CCCDCE; + --color-grey-300: #C1C2C4; + --color-grey-350: #B6B7B9; + --color-grey-400: #AAACAF; + --color-grey-450: #9FA1A4; + --color-grey-500: #94979A; + --color-grey-550: #898C8F; + --color-grey-600: #7E8185; + --color-grey-650: #73767A; + --color-grey-700: #686B70; + --color-grey-750: #5D6065; + --color-grey-800: #52555B; + --color-grey-850: #464B50; + --color-grey-900: #3B4046; + --color-grey-950: #30353B; + --color-grey-1000: #1A1F26; + + --color-primary-red-50: #E08F85; + --color-primary-red-100: #DC796E; + --color-primary-red-200: #D76457; + --color-primary-red-300: #D35040; + --color-primary-red-400: #CE3F2D; + --color-primary-red-500: #DD1405; + --color-primary-red-600: #B72C1C; + --color-primary-red-700: #A22618; + --color-primary-red-800: #8E2114; + --color-primary-red-900: #7A1C10; + + --color-primary-black-50: #EDEDED; + --color-primary-black-100: #B5B7BC; + --color-primary-black-200: #9C9FA4; + --color-primary-black-300: #8F9195; + --color-primary-black-400: #83888E; + --color-primary-black-500: #697077; + --color-primary-black-600: #515861; + --color-primary-black-700: #38404A; + --color-primary-black-800: #1F2834; + --color-primary-black-900: #04101E; + + /* TODO: add secondary colours */ + --color-secondary-cobalt-50: #F5F9FF; + --color-secondary-cobalt-100: #ADC1EA; + --color-secondary-cobalt-200: #82A2DD; + --color-secondary-cobalt-300: #5784D1; + --color-secondary-cobalt-400: #2B67C3; + --color-secondary-cobalt-500: #004BB5; + --color-secondary-cobalt-600: #003C9C; + --color-secondary-cobalt-700: #002E82; + --color-secondary-cobalt-800: #002267; + --color-secondary-cobalt-900: #00174C; + + --color-secondary-ruby-50: #FDD9D9; + --color-secondary-ruby-100: #F9ADAE; + --color-secondary-ruby-200: #F5828B; + --color-secondary-ruby-300: #F0576F; + --color-secondary-ruby-400: #EB2B58; + --color-secondary-ruby-500: #E40046; + --color-secondary-ruby-600: #B70538; + --color-secondary-ruby-700: #8D072B; + --color-secondary-ruby-800: #64081F; + --color-secondary-ruby-900: #3E0713; + --color-secondary-ruby-900-20: #3E071333; + --color-secondary-ruby-900-40: #3E071366; + + --color-opacities-05: rgba(0, 0, 0, 0.05); + --color-opacities-10: rgba(0, 0, 0, 0.1); + --color-opacities-30: rgba(0, 0, 0, 0.3); + --color-opacities-50: rgba(0, 0, 0, 0.5); + --color-opacities-70: rgba(0, 0, 0, 0.7); + --color-opacities-90: rgba(0, 0, 0, 0.9); +} + +/* endregion */ + +/* region Dark Theme */ + +.theme-dark { + --color-bg-page: #1A1F26; + --color-bg-surface-1: #1F242A; + --color-bg-surface-2: #23282F; + --color-bg-surface-3: #2F333C; + --color-bg-transparent: #00000033; + + --color-bg-tinted-peach: #3E2527; + --color-bg-tinted-pink: #402D30; + --color-bg-tinted-pale-pink: #423539; + + --color-heading: #FFFFFF; + --color-copy-primary: #E5E5E5; + --color-copy-secondary: #999999; + + --color-link-enabled: #EDEDED; + --color-link-hovered: #B5B7BC; + --color-link-pressed: #9C94A4; + --color-link-disabled: #686B70; + --color-link-visited: #A3A3A3; + + --color-system-success: #66E79D; + --color-system-alert: #F0CF6D; + --color-system-error: #F5828B; + --color-system-info: #8BC3D9; + + --color-grey-50: #1A1F26; + --color-grey-100: #30353B; + --color-grey-150: #3B4046; + --color-grey-200: #464B50; + --color-grey-250: #52555B; + --color-grey-300: #5D6065; + --color-grey-350: #686B70; + --color-grey-400: #73767A; + --color-grey-450: #7E8185; + --color-grey-500: #898C8F; + --color-grey-550: #94979A; + --color-grey-600: #9FA1A4; + --color-grey-650: #AAACAF; + --color-grey-700: #B6B7B9; + --color-grey-750: #C1C2C4; + --color-grey-800: #CCCDCE; + --color-grey-850: #D7D8D9; + --color-grey-900: #E2E3E3; + --color-grey-950: #EDEDEE; + --color-grey-1000: #F8F8F9; + + --color-primary-red-50: #7A1C10; + --color-primary-red-100: #8E2114; + --color-primary-red-200: #A22618; + --color-primary-red-300: #B72C1C; + --color-primary-red-400: #DD1405; + --color-primary-red-500: #CE3F2D; + --color-primary-red-600: #D35040; + --color-primary-red-700: #D76457; + --color-primary-red-800: #DC796E; + --color-primary-red-900: #E08F85; + + --color-primary-black-50: #04101E; + --color-primary-black-100: #1F2834; + --color-primary-black-200: #38404A; + --color-primary-black-300: #515861; + --color-primary-black-400: #697077; + --color-primary-black-500: #83888E; + --color-primary-black-600: #8F9195; + --color-primary-black-700: #9C9FA4; + --color-primary-black-800: #B5B7BC; + --color-primary-black-900: #EDEDED; + + /* TODO: add secondary colours */ + --color-secondary-cobalt-50: #00174C; + --color-secondary-cobalt-100: #002267; + --color-secondary-cobalt-200: #002E82; + --color-secondary-cobalt-300: #003C9C; + --color-secondary-cobalt-400: #004BB5; + --color-secondary-cobalt-500: #2B67C3; + --color-secondary-cobalt-600: #5784D1; + --color-secondary-cobalt-700: #82A2DD; + --color-secondary-cobalt-800: #ADC1EA; + --color-secondary-cobalt-900: #D9E1F5; + + --color-secondary-ruby-50: #3E0713; + --color-secondary-ruby-100: #64081F; + --color-secondary-ruby-200: #8D072B; + --color-secondary-ruby-300: #B70538; + --color-secondary-ruby-400: #E40046; + --color-secondary-ruby-500: #EB2B58; + --color-secondary-ruby-600: #F0576F; + --color-secondary-ruby-700: #F5828B; + --color-secondary-ruby-800: #F9ADAE; + --color-secondary-ruby-900: #FDD9D9; + --color-secondary-ruby-900-20: #FDD9D933; + --color-secondary-ruby-900-40: #FDD9D966; + + --color-opacities-05: rgba(255, 255, 255, 0.05); + --color-opacities-10: rgba(255, 255, 255, 0.1); + --color-opacities-30: rgba(255, 255, 255, 0.3); + --color-opacities-50: rgba(255, 255, 255, 0.5); + --color-opacities-70: rgba(255, 255, 255, 0.7); + --color-opacities-90: rgba(255, 255, 255, 0.9); +} + +/* endregion */ + +/* endregion */ + +/* region Transition */ + +:root { + --transition-all: all 200ms ease-in-out; + --transition-transform: transform 300ms ease-out; + --transition-opacity: opacity 200ms ease-in-out; + --transition-maxheight: max-height 200ms ease-in-out; +} + +/* endregion */ + +/* region Element Defaults */ + +body { + background-color: var(--color-bg-page); + font: var(--font-copy-md); + color: var(--color-copy-primary); +} + +p { + margin: 0; /* We never want default margin */ +} + +h1, h2, h3, h4, h5, h6 { + color: var(--color-heading); + margin: 0; /* We never want default margin */ +} + +h1 { + font: var(--font-h1); + letter-spacing: var(--font-h1-spacing); +} +h2 { + font: var(--font-h2); + letter-spacing: var(--font-h2-spacing); +} +h3 { + font: var(--font-h3); + letter-spacing: var(--font-h3-spacing); +} +h4 { + font: var(--font-h4); + letter-spacing: var(--font-h4-spacing); +} +h5 { + font: var(--font-h5); + letter-spacing: var(--font-h5-spacing); +} + +h6 { + font: var(--font-h6); + letter-spacing: var(--font-h6-spacing); +} +/* endregion */ diff --git a/sites/mega/static/icons/.gitkeep b/sites/mega/static/icons/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sites/mega/static/icons/check-circle-mto.svg b/sites/mega/static/icons/check-circle-mto.svg new file mode 100644 index 0000000..f790eaa --- /dev/null +++ b/sites/mega/static/icons/check-circle-mto.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/mega/static/icons/cloud-mto.svg b/sites/mega/static/icons/cloud-mto.svg new file mode 100644 index 0000000..a2c234e --- /dev/null +++ b/sites/mega/static/icons/cloud-mto.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/mega/static/icons/cloud-upload-mto.svg b/sites/mega/static/icons/cloud-upload-mto.svg new file mode 100644 index 0000000..b85aeb0 --- /dev/null +++ b/sites/mega/static/icons/cloud-upload-mto.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/mega/static/icons/code-mro.svg b/sites/mega/static/icons/code-mro.svg new file mode 100644 index 0000000..ae9b60a --- /dev/null +++ b/sites/mega/static/icons/code-mro.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/mega/static/icons/database-mto.svg b/sites/mega/static/icons/database-mto.svg new file mode 100644 index 0000000..f063d1c --- /dev/null +++ b/sites/mega/static/icons/database-mto.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/mega/static/icons/folder-mro.svg b/sites/mega/static/icons/folder-mro.svg new file mode 100644 index 0000000..a08009c --- /dev/null +++ b/sites/mega/static/icons/folder-mro.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/mega/static/icons/key-01-mto.svg b/sites/mega/static/icons/key-01-mto.svg new file mode 100644 index 0000000..4fafb71 --- /dev/null +++ b/sites/mega/static/icons/key-01-mto.svg @@ -0,0 +1,4 @@ + + + + diff --git a/sites/mega/static/icons/lock-mto.svg b/sites/mega/static/icons/lock-mto.svg new file mode 100644 index 0000000..256f57c --- /dev/null +++ b/sites/mega/static/icons/lock-mto.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/mega/static/icons/mega.svg b/sites/mega/static/icons/mega.svg new file mode 100644 index 0000000..4fa5d34 --- /dev/null +++ b/sites/mega/static/icons/mega.svg @@ -0,0 +1,4 @@ + + + + diff --git a/sites/mega/static/icons/monitor-mro.svg b/sites/mega/static/icons/monitor-mro.svg new file mode 100644 index 0000000..d179a7f --- /dev/null +++ b/sites/mega/static/icons/monitor-mro.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/mega/static/icons/password-mto.svg b/sites/mega/static/icons/password-mto.svg new file mode 100644 index 0000000..2a1a69c --- /dev/null +++ b/sites/mega/static/icons/password-mto.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/mega/static/icons/search-sro.svg b/sites/mega/static/icons/search-sro.svg new file mode 100644 index 0000000..ceafbe0 --- /dev/null +++ b/sites/mega/static/icons/search-sro.svg @@ -0,0 +1,4 @@ + + + + diff --git a/sites/mega/static/icons/share-mro.svg b/sites/mega/static/icons/share-mro.svg new file mode 100644 index 0000000..69e9e82 --- /dev/null +++ b/sites/mega/static/icons/share-mro.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/mega/static/icons/shield-mro.svg b/sites/mega/static/icons/shield-mro.svg new file mode 100644 index 0000000..34df61e --- /dev/null +++ b/sites/mega/static/icons/shield-mro.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/mega/static/icons/sync-mto.svg b/sites/mega/static/icons/sync-mto.svg new file mode 100644 index 0000000..4c7b6ba --- /dev/null +++ b/sites/mega/static/icons/sync-mto.svg @@ -0,0 +1,4 @@ + + + + diff --git a/sites/mega/static/icons/transfer-it-mto.svg b/sites/mega/static/icons/transfer-it-mto.svg new file mode 100644 index 0000000..1c25180 --- /dev/null +++ b/sites/mega/static/icons/transfer-it-mto.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/sites/mega/static/icons/users-mro.svg b/sites/mega/static/icons/users-mro.svg new file mode 100644 index 0000000..da350e2 --- /dev/null +++ b/sites/mega/static/icons/users-mro.svg @@ -0,0 +1,3 @@ + + + diff --git a/sites/mega/static/js/.gitkeep b/sites/mega/static/js/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sites/mega/static/js/mega.js b/sites/mega/static/js/mega.js new file mode 100644 index 0000000..72a0dbe --- /dev/null +++ b/sites/mega/static/js/mega.js @@ -0,0 +1,6 @@ +document.addEventListener("click", (event) => { + const card = event.target.closest(".plan-card"); + if (!card) return; + document.querySelectorAll(".plan-card").forEach((el) => el.classList.remove("focused")); + card.classList.add("focused"); +}); diff --git a/sites/mega/tasks.jsonl b/sites/mega/tasks.jsonl new file mode 100644 index 0000000..a504057 --- /dev/null +++ b/sites/mega/tasks.jsonl @@ -0,0 +1,18 @@ +{"web_name":"MEGA","id":"MEGA--0","ques":"Browse the MEGA storage product page and find two ways the page says users can access or manage their cloud files.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--1","ques":"Search the help center for recovery key guidance and open the article that explains how to save a recovery key.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--2","ques":"Log in as david.k@test.com. Find an individual storage plan with at least 8 TB of storage that includes MEGA VPN, add it to the cart with yearly billing, and go to checkout.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--3","ques":"Log in as alice.j@test.com, choose the Pro I plan with monthly billing, and complete checkout using Alice's default saved payment method.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--4","ques":"Log in as alice.j@test.com. Compare Pro II and Business Pro, decide which has more included users, add that plan with its minimum included seats to the cart, and stop on the checkout review page.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--5","ques":"Log in as alice.j@test.com. In Cloud drive, find the Atlas launch video in the Media Archive, create a share link, and make sure it is shared with Bob and Carol.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--6","ques":"Log in as alice.j@test.com. Search Cloud drive for media files and determine which MOV file is largest, then open it and mark it as a favorite.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--7","ques":"Log in as alice.j@test.com. Create a folder named Q3 Press Kit inside /Projects/Atlas, then add an uploaded file record named press-summary.pdf with size 4.2 MB in that folder.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--8","ques":"Log in as alice.j@test.com. Add a new MEGA Pass vault entry for the Atlas staging site with username alice_editor, category Client, strong password status, and two-factor authentication enabled.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--9","ques":"Log in as alice.j@test.com. Search the MEGA Pass vault for weak or reused entries and identify which old vendor entry does not have two-factor authentication enabled.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--10","ques":"Open Downloads and find the Windows command-line package rather than the regular sync app. Open its detail page and report its checksum.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--11","ques":"Open Downloads and find the recommended Chrome browser extension for MEGA Pass.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--12","ques":"Log in as bob.c@test.com. Use the Pricing filters to show object storage plans, find the plan that mentions an S3-compatible API, and add it to the cart.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--13","ques":"Browse the Business product page and find the capability related to keeping a team organised through administration or user management.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--14","ques":"Log in as bob.c@test.com and submit a High priority Object storage support ticket asking for help estimating S4 egress for quarterly archives.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--15","ques":"Search Help for ransomware recovery, open the matching article, and identify the first action it recommends before restoring files.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--16","ques":"Log in as alice.j@test.com, update the account profile so the company is Riverlight Studio Labs, and make sure both two-factor authentication and recovery key saved are enabled.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} +{"web_name":"MEGA","id":"MEGA--17","ques":"Log in as bob.c@test.com. Find the flexible plan for variable cloud workloads that starts with 3 TB base storage and transfer, compare it with the fixed S4 storage plan, and add the flexible plan to the cart with yearly billing.","web":"http://localhost:40015/","upstream_url":"https://mega.io/"} diff --git a/sites/mega/templates/.gitkeep b/sites/mega/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sites/mega/templates/404.html b/sites/mega/templates/404.html new file mode 100644 index 0000000..9c9d46e --- /dev/null +++ b/sites/mega/templates/404.html @@ -0,0 +1,3 @@ +{% extends "base.html" %} +{% block title %}Not found{% endblock %} +{% block content %}

Page not found

Use search or the main navigation to continue.

{% endblock %} diff --git a/sites/mega/templates/_plan_card.html b/sites/mega/templates/_plan_card.html new file mode 100644 index 0000000..e434d1b --- /dev/null +++ b/sites/mega/templates/_plan_card.html @@ -0,0 +1,19 @@ +
+ {% if plan.popular %}Popular{% endif %} +

{{ plan.name }}

+

{{ plan.tagline }}

+
+ {% if plan.monthly_price == 0 %}Free{% else %}${{ "%.2f"|format(plan.monthly_price) }}/mo{% endif %} +
+
+
Storage
{{ plan.storage_tb }} TB
+
Transfer
{{ plan.transfer_tb }} TB
+
Users
{{ plan.users_included }}
+
+
+ + + +
+ Details +
diff --git a/sites/mega/templates/account.html b/sites/mega/templates/account.html new file mode 100644 index 0000000..cacf3b6 --- /dev/null +++ b/sites/mega/templates/account.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% block title %}Account{% endblock %} +{% block content %} +
+

{{ current_user.company }}

+

{{ current_user.display_name }}

+

{{ current_user.email }} · {{ current_user.plan.name if current_user.plan else 'No active plan' }}

+
+ +
+
+

Recent files

Open drive
+ {% for item in recent_files %}{{ item.folder }}{{ item.name }}{% endfor %} +
+ +
+

Support

New ticket
+ {% for ticket in tickets %}{{ ticket.status }}{{ ticket.subject }}{% endfor %} +
+
+
+ Edit account + Payment methods +
+{% endblock %} diff --git a/sites/mega/templates/account_edit.html b/sites/mega/templates/account_edit.html new file mode 100644 index 0000000..ece4f41 --- /dev/null +++ b/sites/mega/templates/account_edit.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block title %}Edit Account{% endblock %} +{% block content %} +

Edit account

+
+ {% for field, label in [('display_name','Name'),('phone','Phone'),('company','Company'),('role','Role'),('address_line1','Address line 1'),('address_line2','Address line 2'),('city','City'),('state','State'),('postal_code','Postal code'),('country','Country'),('language','Language'),('timezone','Timezone')] %} + + {% endfor %} + + + +
+{% endblock %} diff --git a/sites/mega/templates/base.html b/sites/mega/templates/base.html new file mode 100644 index 0000000..195b85d --- /dev/null +++ b/sites/mega/templates/base.html @@ -0,0 +1,68 @@ + + + + + + {% block title %}MEGA{% endblock %} + + + + +
+ + MEGA + MEGA + + + + +
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ +
+
+ MEGA +

Encrypted storage, sharing, VPN, password management, and object storage mirrored locally for WebHarbor.

+
+ +
+ + + diff --git a/sites/mega/templates/checkout.html b/sites/mega/templates/checkout.html new file mode 100644 index 0000000..8fc5f8f --- /dev/null +++ b/sites/mega/templates/checkout.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% block title %}Checkout{% endblock %} +{% block content %} +

Checkout

Review your MEGA subscription and select a saved payment method.

+
+
+

{{ plan.name }}

+ + +
+ Payment method + {% for method in methods %} + + {% endfor %} +
+ + Add payment method +
+ +
+{% endblock %} diff --git a/sites/mega/templates/cloud_drive.html b/sites/mega/templates/cloud_drive.html new file mode 100644 index 0000000..b7f5065 --- /dev/null +++ b/sites/mega/templates/cloud_drive.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} +{% block title %}Cloud Drive{% endblock %} +{% block content %} +
+

Cloud drive

+

{{ items|length }} items{% if q %} matching "{{ q }}"{% endif %}

+
+
+ + + +
+
+ +
+
+

New folder

+ + + +
+
+

Upload record

+ + + + + +
+
+
+{% endblock %} diff --git a/sites/mega/templates/contact.html b/sites/mega/templates/contact.html new file mode 100644 index 0000000..bece253 --- /dev/null +++ b/sites/mega/templates/contact.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %}Contact MEGA{% endblock %} +{% block content %} +

We’re here to help

Make an enquiry or report an issue.

+
+ + + + + +
+{% endblock %} diff --git a/sites/mega/templates/download_detail.html b/sites/mega/templates/download_detail.html new file mode 100644 index 0000000..612cbe4 --- /dev/null +++ b/sites/mega/templates/download_detail.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}{{ download.package_name }}{% endblock %} +{% block content %} +
+
+

{{ download.product }} / {{ download.platform }}

+

{{ download.package_name }}

+

{{ download.notes }}

+
+
Version{{ download.version }}
+
Size{{ download.size_mb }} MB
+
Architecture{{ download.architecture }}
+
Checksum{{ download.checksum }}
+
+
+ {{ download.package_name }} +
+{% endblock %} diff --git a/sites/mega/templates/downloads.html b/sites/mega/templates/downloads.html new file mode 100644 index 0000000..9dfda9c --- /dev/null +++ b/sites/mega/templates/downloads.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% block title %}MEGA Downloads{% endblock %} +{% block content %} +

Download MEGA apps

Desktop, mobile, VPN, Pass, CMD, and NAS packages.

+
+ + + +
+
+ {% for d in downloads %} + + {{ d.platform }} + {{ d.product }} · {{ d.platform }} +

{{ d.package_name }}

+

{{ d.version }} · {{ d.size_mb }} MB · {{ d.architecture }}

+ {% if d.recommended %}Recommended{% endif %} +
+ {% endfor %} +
+{% endblock %} diff --git a/sites/mega/templates/file_detail.html b/sites/mega/templates/file_detail.html new file mode 100644 index 0000000..bd225c0 --- /dev/null +++ b/sites/mega/templates/file_detail.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% block title %}{{ item.name }}{% endblock %} +{% block content %} +
+
+

{{ item.folder }}

+

{{ item.name }}

+

{{ item.content_summary }}

+
+
Size{{ item.size_mb|size }}
+
Modified{{ item.modified_at }}
+
Sync{{ item.sync_status }}
+
Backup source{{ item.backup_source or 'None' }}
+
Shared with{{ item.shared_with or 'Not shared' }}
+
Share link{{ item.share_link or 'No link' }}
+
+
+
+

Sharing settings

+ + + + +
+
+{% endblock %} diff --git a/sites/mega/templates/help.html b/sites/mega/templates/help.html new file mode 100644 index 0000000..1e89475 --- /dev/null +++ b/sites/mega/templates/help.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block title %}MEGA Help{% endblock %} +{% block content %} +

Help center

Guides and support for MEGA products.

+
+ + + +
+
+ {% for article in articles %} + + {{ article.category }} · {{ article.updated_at }} + {{ article.title }} +

{{ article.applies_to }} · {{ article.difficulty }}

+
+ {% endfor %} +
+{% endblock %} diff --git a/sites/mega/templates/help_article.html b/sites/mega/templates/help_article.html new file mode 100644 index 0000000..e9a432c --- /dev/null +++ b/sites/mega/templates/help_article.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% block title %}{{ article.title }}{% endblock %} +{% block content %} +
+

{{ article.category }} · {{ article.difficulty }}

+

{{ article.title }}

+

{{ article.body }}

+
+
Applies to{{ article.applies_to }}
+
Updated{{ article.updated_at }}
+
+
+
+

Related articles

+
+ {% for item in related %} + {{ item.category }}{{ item.title }} + {% endfor %} +
+
+{% endblock %} diff --git a/sites/mega/templates/index.html b/sites/mega/templates/index.html new file mode 100644 index 0000000..847548e --- /dev/null +++ b/sites/mega/templates/index.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} +{% block title %}MEGA: Protect your online privacy{% endblock %} +{% block content %} +
+
+

The Privacy Company

+

Online privacy for everyone, with encrypted storage by default

+

Store and share files, collaborate, browse privately, and protect passwords from one MEGA account.

+ +
+
+ Encrypted cloud storage +
+
+ +
+
+

Do more with MEGA

+ Get apps +
+
+ {% for page in products %} + + {{ page.title }} + {{ page.section }} +

{{ page.title }}

+

{{ page.summary }}

+
+ {% endfor %} +
+
+ +
+
+

Popular plans

+ All pricing +
+
+ {% for plan in featured_plans %} + {% include "_plan_card.html" %} + {% endfor %} +
+
+ +
+
+

Security that is always on

+

MEGA surfaces recovery keys, two-factor authentication, sharing controls, and version recovery as everyday account tools.

+ Security and privacy +
+ Security and privacy +
+ +
+
+

Help topics

+ Open help center +
+
+ {% for article in help_articles %} + + {{ article.category }} + {{ article.title }} + + {% endfor %} +
+
+{% endblock %} diff --git a/sites/mega/templates/login.html b/sites/mega/templates/login.html new file mode 100644 index 0000000..22cf766 --- /dev/null +++ b/sites/mega/templates/login.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block title %}Log in{% endblock %} +{% block content %} +
+
+

Log in to MEGA

+ + + +

Benchmark users use password TestPass123!.

+
+
+{% endblock %} diff --git a/sites/mega/templates/order_detail.html b/sites/mega/templates/order_detail.html new file mode 100644 index 0000000..20f123f --- /dev/null +++ b/sites/mega/templates/order_detail.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% block title %}Order {{ order.order_number }}{% endblock %} +{% block content %} +

{{ order.order_number }}

{{ order.status|title }} subscription order from {{ order.created_at }}.

+
+
Plan{{ order.plan.name }}
+
Billing{{ order.billing_cycle }}
+
Seats{{ order.seats }}
+
Total${{ "%.2f"|format(order.total) }}
+
+{% endblock %} diff --git a/sites/mega/templates/payment_methods.html b/sites/mega/templates/payment_methods.html new file mode 100644 index 0000000..8198c79 --- /dev/null +++ b/sites/mega/templates/payment_methods.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% block title %}Payment Methods{% endblock %} +{% block content %} +

Payment methods

+
+
+ {% for method in methods %} +
{{ method.card_type }} · {{ method.billing_country }}{{ method.label }} ending {{ method.last4 }}{% if method.is_default %} · Default{% endif %}
+ {% endfor %} +
+
+

Add card

+ + + + + + + + +
+
+{% endblock %} diff --git a/sites/mega/templates/plan_detail.html b/sites/mega/templates/plan_detail.html new file mode 100644 index 0000000..da4ea75 --- /dev/null +++ b/sites/mega/templates/plan_detail.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block title %}{{ plan.name }}{% endblock %} +{% block content %} +
+
+

{{ plan.audience|title }} / {{ plan.category|title }}

+

{{ plan.name }}

+

{{ plan.description }}

+
    + {% for feature in plan.get_features() %}
  • {{ feature }}
  • {% endfor %} +
+
+ +
+
+

Similar options

+
+ {% for plan in alternatives %} + {% include "_plan_card.html" %} + {% endfor %} +
+
+{% endblock %} diff --git a/sites/mega/templates/pricing.html b/sites/mega/templates/pricing.html new file mode 100644 index 0000000..86bf2f9 --- /dev/null +++ b/sites/mega/templates/pricing.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block title %}Compare MEGA plans{% endblock %} +{% block content %} +
+

Pricing

+

Compare plans for storage, VPN, Pass, business, and S4

+

Filter by audience or product family, then choose a plan to checkout.

+
+
+ + + + +
+
+ {% for plan in plans %} + {% include "_plan_card.html" %} + {% endfor %} +
+{% endblock %} diff --git a/sites/mega/templates/product_page.html b/sites/mega/templates/product_page.html new file mode 100644 index 0000000..147349a --- /dev/null +++ b/sites/mega/templates/product_page.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% block title %}{{ page.title }}{% endblock %} +{% block content %} +
+
+

{{ page.section }}

+

{{ page.title }}

+

{{ page.summary }}

+ +
+
{{ page.title }}
+
+
+
+

What you can do

+

{{ page.body }}

+
    {% for item in page.get_highlights() %}
  • {{ item }}
  • {% endfor %}
+
+
+ {% for row in page.get_faq() %} +

{{ row.q }}

{{ row.a }}

+ {% endfor %} +
+
+{% endblock %} diff --git a/sites/mega/templates/register.html b/sites/mega/templates/register.html new file mode 100644 index 0000000..b5a211d --- /dev/null +++ b/sites/mega/templates/register.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block content %} +
+
+

Create account

+ + + + + +
+
+{% endblock %} diff --git a/sites/mega/templates/search.html b/sites/mega/templates/search.html new file mode 100644 index 0000000..2272293 --- /dev/null +++ b/sites/mega/templates/search.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block title %}Search{% endblock %} +{% block content %} +

Search MEGA

Results for "{{ q }}".

+{% for heading, rows, endpoint in [ + ('Plans', plans, 'plan_detail'), + ('Products', pages, 'product_page'), + ('Help', articles, 'help_article') +] %} +
+

{{ heading }}

+
+ {% for row in rows %} + {% if heading == 'Plans' %} + {{ row.category }}{{ row.name }}

{{ row.tagline }}

+ {% elif heading == 'Products' %} + {{ row.section }}{{ row.title }}

{{ row.summary }}

+ {% else %} + {{ row.category }}{{ row.title }} + {% endif %} + {% endfor %} +
+
+{% endfor %} +
+

Downloads

+ +
+{% if current_user.is_authenticated %} +
+

Your files

+
{% for f in files %}{{ f.folder }}{{ f.name }}{% endfor %}
+
+
+

Your vault

+
{% for v in vault_items %}
{{ v.category }} · {{ v.strength }}{{ v.title }}
{% endfor %}
+
+{% endif %} +{% endblock %} diff --git a/sites/mega/templates/ticket_detail.html b/sites/mega/templates/ticket_detail.html new file mode 100644 index 0000000..4fa7e02 --- /dev/null +++ b/sites/mega/templates/ticket_detail.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block title %}{{ ticket.ticket_number }}{% endblock %} +{% block content %} +
+

{{ ticket.category }} · {{ ticket.priority }}

+

{{ ticket.subject }}

+

{{ ticket.message }}

+
+
Ticket{{ ticket.ticket_number }}
+
Status{{ ticket.status }}
+
Created{{ ticket.created_at }}
+
+
+{% endblock %} diff --git a/sites/mega/templates/vault.html b/sites/mega/templates/vault.html new file mode 100644 index 0000000..e10854d --- /dev/null +++ b/sites/mega/templates/vault.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% block title %}MEGA Pass Vault{% endblock %} +{% block content %} +

MEGA Pass

Encrypted password vault entries for {{ current_user.display_name }}.

+
+ + +
+
+
+ {% for item in items %} +
+ {{ item.category }} + {{ item.title }} + {{ item.username }} + {{ item.strength }} + {{ '2FA' if item.two_factor else 'No 2FA' }} +
+ {% endfor %} +
+
+

Add vault entry

+ + + + + + + + +
+
+{% endblock %} diff --git a/websyn_start.sh b/websyn_start.sh index 72defad..bc4c836 100644 --- a/websyn_start.sh +++ b/websyn_start.sh @@ -1,11 +1,11 @@ #!/bin/bash -# WebSyn startup: launch all 12 mirror sites, then exec the original CMD. +# WebSyn startup: launch all mirror sites, then exec the original CMD. # This preserves the base image's browser env server (port 8100) as PID 1. set -e SITES=(allrecipes amazon apple arxiv bbc_news booking github google_flights google_map google_search huggingface wolfram_alpha - cambridge_dictionary coursera espn) + cambridge_dictionary coursera espn mega) BASE_PORT=40000 PID_DIR=/tmp/websyn_pids mkdir -p "$PID_DIR" @@ -17,7 +17,8 @@ for d in "${SITES[@]}"; do cp -a "/opt/WebSyn/$d/instance_seed" "/opt/WebSyn/$d/instance" done -echo "[WebSyn] Starting 15 sites on ports ${BASE_PORT}-$((BASE_PORT + 14))..." +SITE_COUNT=${#SITES[@]} +echo "[WebSyn] Starting ${SITE_COUNT} sites on ports ${BASE_PORT}-$((BASE_PORT + SITE_COUNT - 1))..." for i in "${!SITES[@]}"; do site="${SITES[$i]}" port=$((BASE_PORT + i)) @@ -51,8 +52,8 @@ except Exception: exit(1) ready=$((ready + 1)) fi done - echo " [${elapsed}/${max_wait}s] ${ready}/15 sites ready" - if [ $ready -eq 15 ]; then + echo " [${elapsed}/${max_wait}s] ${ready}/${SITE_COUNT} sites ready" + if [ $ready -eq $SITE_COUNT ]; then break fi done