diff --git a/.codio b/.codio
index 714f1b7f..7cee24a5 100644
--- a/.codio
+++ b/.codio
@@ -4,18 +4,14 @@
"commands": {
"Make": "clear && make html",
"Make Instructor": "clear && make html TYPE=instructor",
- "Make Student": "clear && make html TYPE=student",
- "Start Ungit":"ungit --port=4000 --ungitBindIp 0.0.0.0"
+ "Make Student": "clear && make html TYPE=student"
},
// Preview button configuration
"preview": {
- "Instructor": "https://{{domain}}/build/html/index.html",
+ "Instructor": "https://{{domain}}/build/html/source/index.html",
"Preview": "https://{{domain}}/build/html/{{path}}/{{filename_no_ext}}.html",
- "Student": "https://{{domain}}/build/html/student.html",
- "Visit Ungit": "https://{{domain4000}}/#/repository?path=%2Fhome%2Fcodio%2Fworkspace"
-
-
+ "Student": "https://{{domain}}/build/html/student-source/student.html"
},
// Debugger target button configuration
"debugger": [{"type":"PYTHON3","command":"{{filepath}}","before":"","uuid":"6c228c3f-596c-c44a-399f-5d86a7e17bf9","name":"Debug"},{"type":"GDB","command":"/codio-docs/","before":"","uuid":"76725fc4-3220-4a33-f739-614ebe121e67","name":"doc test"}]
-}
\ No newline at end of file
+}
diff --git a/Makefile b/Makefile
index 327f0695..c869e054 100644
--- a/Makefile
+++ b/Makefile
@@ -6,6 +6,8 @@ SPHINXOPTS =
SPHINXBUILD = sphinx-build
SOURCEDIR = source
BUILDDIR = build
+BUILDDIRSTU = build
+BUILDTARGET = -M
# Put it first so that "make" without argument is like "make help".
help:
@@ -18,17 +20,24 @@ ifeq ($(TYPE),)
TYPE := both
endif
+ifeq ($(PWD),/home/codio/workspace)
+BUILDTARGET := -b
+BUILDDIR := build/html/source
+BUILDDIRSTU := build/html/student-source
+endif
+
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
rm -rf "$(BUILDDIR)"
+ rm -rf "$(BUILDDIRSTU)"
ifeq ($(TYPE),student)
- @$(SPHINXBUILD) -M $@ "student-$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+ @$(SPHINXBUILD) "$(BUILDTARGET)" $@ "student-$(SOURCEDIR)" "$(BUILDDIRSTU)" $(SPHINXOPTS) $(O)
else ifeq ($(TYPE),instructor)
- @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+ @$(SPHINXBUILD) "$(BUILDTARGET)" $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
else ifeq ($(TYPE),both)
- @$(SPHINXBUILD) -M $@ "student-$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
- @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+ @$(SPHINXBUILD) "$(BUILDTARGET)" $@ "student-$(SOURCEDIR)" "$(BUILDDIRSTU)" $(SPHINXOPTS) $(O)
+ @$(SPHINXBUILD) "$(BUILDTARGET)" $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
else
$(error Invalid TYPE='$(TYPE)'. Use TYPE=student, TYPE=instructor, or TYPE=both)
endif
\ No newline at end of file
diff --git a/source/_static/css/custom_body.css b/source/_static/css/custom_body.css
index ae5fae8c..067e3fdc 100644
--- a/source/_static/css/custom_body.css
+++ b/source/_static/css/custom_body.css
@@ -1,138 +1,111 @@
/* =========================================
- Codio Docs — Home (index) only
- Pure theme background, centered layout
+ Codio Docs — Landing pages (Instructor & Student)
+ Layout resets + shared design tokens
========================================= */
-/* Hide sidebars on the homepage and center content */
-.index-page .bd-sidebar-primary,
-.index-page .bd-sidebar-secondary { display: none !important; }
+/* Hide the sidebars and unbox the layout on the landing pages */
+.landing-page .bd-sidebar-primary,
+.landing-page .bd-sidebar-secondary {
+ display: none !important;
+}
-.index-page .bd-content,
-.index-page .bd-article-container {
- max-width: 100%;
+.landing-page .bd-content,
+.landing-page .bd-article-container {
+ max-width: 1200px;
margin-inline: auto !important;
- padding-inline: 1rem;
+ padding-inline: clamp(1rem, 5vw, 2.5rem);
+ background: transparent !important;
}
-/* Make every wrapper transparent so there is NO boxed panel */
-.index-page,
-.index-page .bd-main,
-.index-page .bd-content,
-.index-page .bd-article-container,
-.index-page .bd-article {
+.landing-page,
+.landing-page .bd-main,
+.landing-page .bd-content,
+.landing-page .bd-article,
+.landing-page .bd-article-container {
background: transparent !important;
}
-/* ------- Theme tokens (only for the homepage components) ------- */
-html[data-theme="dark"] .index-page,
-[data-theme="dark"] .index-page {
- --text: #e6eaf2;
- --muted: #9aa3b2;
- --border: rgba(255,255,255,0.14);
- --surface-hover: rgba(255,255,255,0.06);
- --brand: #6ee7ff;
- --brand-strong: #3dd8ff;
- --ring: #9feaff;
- --shadow: 0 10px 26px rgba(3,8,48,0.35);
+.landing-page {
+ color-scheme: light dark;
+ color: var(--landing-text);
+ --landing-text: #0f172a;
+ --landing-muted: #475569;
+ --landing-border: rgba(15, 23, 42, 0.12);
+ --landing-surface: rgba(255, 255, 255, 0.85);
+ --landing-surface-hover: rgba(14, 165, 233, 0.08);
+ --landing-accent: #0284c7;
+ --landing-accent-strong: #0369a1;
+ --landing-ring: rgba(14, 165, 233, 0.45);
+ --landing-card-shadow: 0 16px 28px rgba(15, 23, 42, 0.12);
+ --landing-card-shadow-dark: 0 22px 40px rgba(8, 47, 73, 0.45);
+ --landing-button-shadow: 0 14px 28px rgba(2, 132, 199, 0.18);
+ --landing-icon-shadow: 0 14px 28px rgba(2, 132, 199, 0.24);
+ --landing-hero-shadow: 0 24px 48px rgba(15, 118, 210, 0.12);
+ --landing-hero-from: #eff6ff;
+ --landing-hero-to: #dbeafe;
}
-html[data-theme="light"] .index-page,
-[data-theme="light"] .index-page {
- --text: #0b1020;
- --muted: #475569;
- --border: rgba(0,0,0,0.12);
- --surface-hover: rgba(2,132,199,0.05);
- --brand: #0ea5e9;
- --brand-strong: #0284c7;
- --ring: #38bdf8;
- --shadow: 0 10px 24px rgba(2,132,199,0.08);
+html[data-theme="dark"] .landing-page,
+[data-theme="dark"] .landing-page {
+ --landing-text: #e5edff;
+ --landing-muted: #94a3b8;
+ --landing-border: rgba(148, 163, 184, 0.18);
+ --landing-surface: rgba(15, 23, 42, 0.68);
+ --landing-surface-hover: rgba(56, 189, 248, 0.18);
+ --landing-accent: #38bdf8;
+ --landing-accent-strong: #0ea5e9;
+ --landing-ring: rgba(56, 189, 248, 0.6);
+ --landing-card-shadow: var(--landing-card-shadow-dark);
+ --landing-button-shadow: 0 18px 36px rgba(15, 118, 210, 0.36);
+ --landing-icon-shadow: 0 20px 38px rgba(14, 165, 233, 0.45);
+ --landing-hero-shadow: 0 26px 52px rgba(2, 132, 199, 0.32);
+ --landing-hero-from: #0f172a;
+ --landing-hero-to: #111827;
}
-/* Fallback to OS pref if the site doesn't set data-theme */
-@media (prefers-color-scheme: light) {
- html:not([data-theme="dark"]) .index-page {
- --text: #0b1020;
- --muted: #475569;
- --border: rgba(0,0,0,0.12);
- --surface-hover: rgba(2,132,199,0.05);
- --brand: #0ea5e9;
- --brand-strong: #0284c7;
- --ring: #38bdf8;
- --shadow: 0 10px 24px rgba(2,132,199,0.08);
+@media (prefers-color-scheme: dark) {
+ html:not([data-theme="light"]) .landing-page {
+ --landing-text: #e5edff;
+ --landing-muted: #94a3b8;
+ --landing-border: rgba(148, 163, 184, 0.18);
+ --landing-surface: rgba(15, 23, 42, 0.68);
+ --landing-surface-hover: rgba(56, 189, 248, 0.18);
+ --landing-accent: #38bdf8;
+ --landing-accent-strong: #0ea5e9;
+ --landing-ring: rgba(56, 189, 248, 0.6);
+ --landing-card-shadow: var(--landing-card-shadow-dark);
+ --landing-button-shadow: 0 18px 36px rgba(15, 118, 210, 0.36);
+ --landing-icon-shadow: 0 20px 38px rgba(14, 165, 233, 0.45);
+ --landing-hero-shadow: 0 26px 52px rgba(2, 132, 199, 0.32);
+ --landing-hero-from: #0f172a;
+ --landing-hero-to: #111827;
}
}
-/* ------- Hero ------- */
-.home-hero { padding: 5rem 1rem 1.5rem; text-align: center; }
-.home-title { color: var(--text); font-size: clamp(2rem, 2.8vw + 1rem, 3rem); margin: 0 0 .6rem; }
-.home-subtitle { color: var(--muted); margin: 0 auto 1.8rem; max-width: 62ch; }
-
-/* (Search form is commented out in your HTML, but styles left here in case you re-enable) */
-.home-search {
- display: inline-flex; gap: .5rem; align-items: center;
- background: transparent; /* no panel */
- border: 1px solid var(--border);
- border-radius: 999px; padding: .5rem;
- box-shadow: none;
-}
-.home-search input[type="search"] {
- min-width: clamp(260px, 42vw, 560px);
- background: transparent; border: none; color: var(--text);
- outline: none; padding: .75rem 1rem; font-size: 1rem;
-}
-.home-search input::placeholder { color: var(--muted); }
-.search-btn {
- display: inline-flex; align-items: center; gap: .5rem;
- border: 1px solid transparent;
- background: linear-gradient(180deg, var(--brand), var(--brand-strong));
- color: #001; font-weight: 700; padding: .7rem 1rem;
- border-radius: 999px; cursor: pointer;
- box-shadow: 0 10px 24px rgba(0, 190, 255, .18);
-}
-.search-btn:focus-visible { outline: 3px solid var(--ring); outline-offset: 2px; }
-.search-btn .icon { width: 20px; height: 20px; }
-
-/* ------- Sections ------- */
-.home-main { padding: .5rem 0 3rem; }
-.home-container { max-width: 1100px; margin-inline: auto; padding-inline: 1rem; }
-.section { margin-top: 1.25rem; }
-.section-title { color: var(--muted); font-weight: 600; letter-spacing: .2px; margin: 0 0 1rem; }
-
-/* ------- Cards: transparent by default (no fill), subtle outline ------- */
-.card-grid {
- --min: 260px;
- display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--min), 1fr));
- gap: 1rem; align-items: stretch;
-}
-.card {
- display: grid; grid-template-rows: auto auto 1fr auto; gap: .5rem;
- padding: 1rem; border-radius: 16px; text-decoration: none; color: var(--text);
- background: transparent; /* <- no box */
- border: 1px solid var(--border);
- box-shadow: none;
- transition: transform .2s ease, background-color .15s ease, border-color .15s ease;
-}
-.card:hover { transform: translateY(-2px); background: var(--surface-hover); border-color: var(--brand); }
-.card:focus-visible { outline: 3px solid var(--ring); outline-offset: 2px; }
-
-.card-icon {
- width: 36px; height: 36px; display: grid; place-items: center;
- border-radius: 12px;
- background: linear-gradient(180deg, var(--brand), var(--brand-strong));
- box-shadow: 0 8px 18px rgba(0, 190, 255, .2);
+@media (prefers-color-scheme: light) {
+ html:not([data-theme="dark"]) .landing-page {
+ --landing-text: #0f172a;
+ --landing-muted: #475569;
+ --landing-border: rgba(15, 23, 42, 0.12);
+ --landing-surface: rgba(255, 255, 255, 0.85);
+ --landing-surface-hover: rgba(14, 165, 233, 0.08);
+ --landing-accent: #0284c7;
+ --landing-accent-strong: #0369a1;
+ --landing-ring: rgba(14, 165, 233, 0.45);
+ --landing-card-shadow: 0 16px 28px rgba(15, 23, 42, 0.12);
+ --landing-button-shadow: 0 14px 28px rgba(2, 132, 199, 0.18);
+ --landing-icon-shadow: 0 14px 28px rgba(2, 132, 199, 0.24);
+ --landing-hero-shadow: 0 24px 48px rgba(15, 118, 210, 0.12);
+ --landing-hero-from: #eff6ff;
+ --landing-hero-to: #dbeafe;
+ }
}
-.card-icon svg { width: 22px; height: 22px; fill: #001; }
-
-.card-title { font-size: 1.05rem; margin: .25rem 0; }
-.card-desc { margin: 0; color: var(--muted); }
-.card-cta { margin-top: .5rem; font-weight: 700; color: var(--brand); display: inline-flex; gap: .35rem; }
-.card-cta::after { content: "›"; font-size: 1.2em; transform: translateY(-1px); }
-/* Reduced motion */
-@media (prefers-reduced-motion: reduce) { .card, .search-btn { transition: none; } }
+.section { scroll-margin-top: 5rem; }
-/* Small screens */
-@media (max-width: 480px) {
- .home-search { width: 100%; }
- .home-search input[type="search"] { min-width: 0; width: 100%; }
+/* Retain backwards-compat class hooks */
+.index-page .home-container,
+.student-page .home-container {
+ max-width: 1100px;
+ margin-inline: auto;
}
diff --git a/source/_static/css/custom_index.css b/source/_static/css/custom_index.css
index 729bfd77..3492c794 100644
--- a/source/_static/css/custom_index.css
+++ b/source/_static/css/custom_index.css
@@ -1,12 +1,160 @@
-/* Slight width nudge only (kept minimal) */
-.index-page .home-container { max-width: 100%; }
-
-/* Ensure any Bootstrap-like main column stays centered on the homepage */
-@media (min-width: 1200px) {
- .index-page .col-xl-8 {
- flex: 0 0 91.66667% !important;
- max-width: 91.66667% !important;
- margin-left: auto !important;
- margin-right: auto !important;
- }
+/* =========================================
+ Shared landing page components
+ Hero, cards, and responsive utilities
+ ========================================= */
+
+.home-container { padding-inline: clamp(1.5rem, 5vw, 3rem); }
+
+.landing-hero {
+ padding: clamp(2rem, 4vw, 5.25rem) clamp(1.5rem, 4vw, 2.5rem);
+ background: linear-gradient(135deg, var(--landing-hero-from), var(--landing-hero-to));
+ border-radius: clamp(24px, 5vw, 40px);
+ position: relative;
+ overflow: hidden;
+ box-shadow: var(--landing-hero-shadow);
+ margin-inline: clamp(1rem, 4vw, 2.5rem);
+}
+
+.hero-inner { position: relative; text-align: center; max-width: 52rem; margin-inline: auto; }
+.hero-eyebrow {
+ font-size: .8rem;
+ letter-spacing: .32em;
+ text-transform: uppercase;
+ font-weight: 600;
+ color: var(--landing-muted);
+ margin-bottom: 0.85rem;
+}
+.hero-title { font-size: clamp(2.3rem, 2.4vw + 1.5rem, 3.4rem); line-height: 1.1; margin: 0 0 1.15rem; color: var(--landing-text); }
+.hero-lede { color: var(--landing-muted); font-size: clamp(1.05rem, 1vw + .9rem, 1.25rem); margin: 0 auto clamp(1.6rem, 3.5vw, 2.3rem); max-width: 62ch; }
+
+.hero-actions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: .85rem;
+}
+
+.hero-button,
+.hero-link {
+ display: inline-flex;
+ align-items: center;
+ gap: .5rem;
+ border-radius: 999px;
+ font-weight: 600;
+ text-decoration: none;
+ transition: transform .2s ease, box-shadow .2s ease, background-color .2s ease, border-color .2s ease;
+}
+
+.hero-button {
+ padding: .85rem 1.5rem;
+ background: linear-gradient(135deg, var(--landing-accent), var(--landing-accent-strong));
+ color: #f8fafc;
+ box-shadow: var(--landing-button-shadow);
+}
+
+.hero-link {
+ padding: .82rem 1.3rem;
+ background: transparent;
+ border: 1px solid var(--landing-border);
+ color: var(--landing-text);
+}
+
+.hero-button:hover { transform: translateY(-1px); box-shadow: 0 18px 36px rgba(14, 165, 233, .22); }
+.hero-link:hover { transform: translateY(-1px); border-color: var(--landing-accent); color: var(--landing-accent); }
+
+.hero-button:focus-visible,
+.hero-link:focus-visible,
+.card:focus-visible {
+ outline: 3px solid var(--landing-ring);
+ outline-offset: 3px;
+}
+
+.home-main { padding-block: clamp(2.5rem, 4vw, 4rem); }
+.section { margin-top: clamp(1.5rem, 2vw, 2.5rem); }
+.section-title { font-size: .95rem; text-transform: uppercase; letter-spacing: .28em; color: var(--landing-muted); margin-bottom: 1.5rem; text-align: center; }
+
+.card-grid {
+ --card-min: 260px;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(var(--card-min), 1fr));
+ gap: clamp(1rem, 2vw, 1.8rem);
+ align-items: stretch;
+}
+
+.card-grid.cards-4 {
+ --card-min: 280px;
+ max-width: min(880px, 100%);
+ margin-inline: auto;
+}
+
+.card-grid.cards-6 { --card-min: 220px; }
+
+.card {
+ position: relative;
+ display: grid;
+ grid-template-rows: auto auto 1fr auto;
+ gap: .65rem;
+ padding: clamp(1.25rem, 2vw, 1.75rem);
+ border-radius: 20px;
+ text-decoration: none;
+ color: var(--landing-text);
+ background: var(--landing-surface);
+ border: 1px solid color-mix(in srgb, var(--landing-border) 70%, transparent);
+ box-shadow: var(--landing-card-shadow);
+ backdrop-filter: blur(6px);
+ transition: transform .22s ease, background-color .18s ease, border-color .18s ease, box-shadow .2s ease;
+}
+
+.card:hover {
+ transform: translateY(-4px);
+ border-color: color-mix(in srgb, var(--landing-accent) 55%, transparent);
+ box-shadow: 0 24px 44px rgba(14, 165, 233, 0.22);
+}
+
+.card-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 14px;
+ display: grid;
+ place-items: center;
+ background: linear-gradient(135deg, var(--landing-accent), var(--landing-accent-strong));
+ box-shadow: var(--landing-icon-shadow);
+}
+
+.card-icon svg { width: 24px; height: 24px; fill: #f8fafc; }
+
+.card-title { font-size: 1.15rem; margin: 0; }
+.card-desc { margin: 0; color: var(--landing-muted); line-height: 1.55; }
+.card-cta {
+ display: inline-flex;
+ align-items: center;
+ gap: .4rem;
+ margin-top: .2rem;
+ font-weight: 600;
+ color: var(--landing-accent);
+}
+
+.card-cta::after { content: "→"; font-size: 1.1em; transition: transform .18s ease; }
+.card:hover .card-cta::after { transform: translateX(3px); }
+
+/* Motion & accessibility */
+@media (prefers-reduced-motion: reduce) {
+ .hero-button,
+ .hero-link,
+ .card { transition: none; }
+ .card:hover .card-cta::after { transform: none; }
+}
+
+/* Responsive tweaks */
+@media (max-width: 720px) {
+ .landing-hero { border-radius: clamp(18px, 8vw, 30px); }
+ .hero-actions { flex-direction: column; }
+ .hero-link, .hero-button { width: 100%; justify-content: center; }
+ .section-title { text-align: left; }
+}
+
+@media (max-width: 520px) {
+ .home-container { padding-inline: clamp(1rem, 6vw, 1.5rem); }
+ .landing-hero { padding-inline: clamp(1.2rem, 5vw, 1.8rem); margin-inline: 0; }
+ .card-grid { grid-template-columns: 1fr; }
}
diff --git a/source/_templates/index.html b/source/_templates/index.html
index 2836d239..c710b513 100644
--- a/source/_templates/index.html
+++ b/source/_templates/index.html
@@ -2,44 +2,24 @@
{% set title = _('Codio') %}
{% block body %}
-
-
-
-
-
-
-
+
+
-
-
+
-
-
-
+
Instructor guides
+
diff --git a/source/admin.rst b/source/admin.rst
index 437cd46f..ba9c86e3 100644
--- a/source/admin.rst
+++ b/source/admin.rst
@@ -13,9 +13,11 @@ Admin
instructors/admin/integration/intro
instructors/admin/organization/organisation
- instructors/admin/orgbilling
common/settings/settings
+ instructors/admin/sandboxes
+ instructors/admin/orgbilling
instructors/admin/legal
+
Integrating with your LMS system (Canvas etc.)
----------------------------------------------
diff --git a/source/instructors/admin/integration/lti1-3Canvas.rst b/source/instructors/admin/integration/lti1-3Canvas.rst
index c98eb841..8a8078bd 100644
--- a/source/instructors/admin/integration/lti1-3Canvas.rst
+++ b/source/instructors/admin/integration/lti1-3Canvas.rst
@@ -70,7 +70,7 @@ You only need to configure:
If you use the JSON configuration URL method, skip to **Part 2**. To manually configure everything, continue with the steps below.
(Step 6, Option 2) Completing Canvas Steps Manually
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Complete the **Key Name, Title** and **Description** fields. Make sure to set the **method** to **Manual Entry**.
@@ -118,7 +118,7 @@ Complete the **Key Name, Title** and **Description** fields. Make sure to set th
Link Selection and Assignment Selection
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: /img/canvaslinkassignmentselect.png
:alt: Canvas Link Selection placement
diff --git a/source/instructors/admin/sandboxes.rst b/source/instructors/admin/sandboxes.rst
new file mode 100644
index 00000000..43249d33
--- /dev/null
+++ b/source/instructors/admin/sandboxes.rst
@@ -0,0 +1,191 @@
+.. meta::
+ :description: Sandboxes allow instructors to set safe AWS environments.
+
+.. _sandboxes:
+
+Sandboxes
+=========
+
+About Sandboxes
+---------------
+
+Sandboxes provide time‑boxed, ephemeral **AWS Management Console environments** that expire automatically. A sandbox's permissions are defined by a template and can range from full administrator access to least‑privileged roles, depending on need. When a sandbox's duration ends, access is revoked and all resources created inside the sandbox are cleaned up automatically—no user action required.
+
+.. important::
+
+ **Sandboxes are not enabled by default.** They require a signed agreement because they incur additional costs for your organization. To enable Sandboxes for your organization, please reach out to Support.
+
+.. note::
+
+ **AWS region:** Sandboxes currently run in ``us-east-1`` only. Additional regions can be added if needed.
+
+Key Concepts
+------------
+
+.. list-table::
+ :widths: 20 80
+ :header-rows: 1
+
+ * - Concept
+ - Description
+ * - **Sandbox**
+ - A short-lived AWS environment defined by a ``sandbox.yml`` template. It grants time-boxed access and (optionally) provisions resources for hands-on work.
+ * - **Type**
+ - The sandbox runtime. Two types are illustrated in the examples below:
+
+ - ``aws_cloud`` — console-first sessions that assume an AWS role defined by a policy (optionally orchestrated by an Infrastructure-as-Code engine).
+
+ - ``aws_ec2`` — a session that provisions an EC2 instance with parameters like instance type, image, disk size, and connection mode (``ssh`` or ``vnc``).
+ * - **Lifetime**
+ - The initial duration of the sandbox (e.g., ``15m``, ``30m``). Access and resources automatically expire at the end of the lifetime.
+ * - **Lifetime Extension**
+ - Optional, on-demand increments (e.g., ``5m``, ``10m``) that can be applied to a running sandbox, up to ``lifetime_max``.
+ * - **Policy**
+ - The permissions definition attached to the sandbox (for example, an IAM policy file like ``lab.policy``). This controls the permission level from full to least-privileged.
+ * - **Provision Engine**
+ - The engine used to orchestrate resources defined by the sandbox (e.g., ``terraform``, ``bash``, or CloudFormation).
+ * - **Billing Limit**
+ - A numeric cap used by the platform to help control costs attributed to the sandbox (e.g., ``5.0``).
+ * - **Parameters**
+ - Resource-specific inputs for the sandbox type (for example, EC2 instance settings: ``instance_type``, ``image``, ``volume_size``, ``connection_mode``).
+ * - **Region**
+ - The AWS region in which sandboxes are created. Currently fixed to ``us-east-1``.
+
+Lifecycle, Duration, and Cleanup
+--------------------------------
+
+- **Start:** Launch a sandbox from a ``sandbox.yml`` template. The environment and its access are created for the configured ``lifetime``.
+- **Extend:** While running, you may extend the lifetime in ``lifetime_extension`` increments, **not exceeding** ``lifetime_max``.
+- **Expire & Clean:** When the lifetime ends, access is revoked and resources created by the sandbox are cleaned up automatically—no user action required.
+
+Sandbox Configuration (``sandbox.yml``)
+---------------------------------------
+
+Define sandboxes in YAML. The best starting point is using this command to get four **working examples** you can adapt:
+
+.. code-block:: bash
+
+ git clone https://github.com/codio-content/sandboxes_examples.git .
+
+.. important::
+
+ The command above will copy the repository to your current directory. Make sure your folder is empty.
+
+Configuration Reference
+-----------------------
+
+Top-level keys
+~~~~~~~~~~~~~~
+
+.. list-table::
+ :widths: 20 15 10 55
+ :header-rows: 1
+
+ * - Key
+ - Type
+ - Required
+ - Description
+ * - ``version``
+ - string
+ - Yes
+ - Schema version of the sandbox definition (e.g., ``"1.0"`` or ``1.0``).
+ * - ``type``
+ - enum
+ - Yes
+ - Sandbox runtime type. Supported in examples: ``aws_cloud`` or ``aws_ec2``.
+ * - ``settings``
+ - mapping
+ - Yes
+ - Configuration block that defines lifetime behavior, permissions, provisioning engine, and resource parameters.
+
+``settings`` (common)
+~~~~~~~~~~~~~~~~~~~~~
+
+.. list-table::
+ :widths: 22 14 10 54
+ :header-rows: 1
+
+ * - Key
+ - Type
+ - Required
+ - Description
+ * - ``lifetime``
+ - string
+ - Yes
+ - Initial duration of the sandbox (e.g., ``"15m"``, ``"30m"``). Sandboxes are **ephemeral** and expire after this time.
+ * - ``lifetime_extension``
+ - string
+ - No
+ - Increment applied when extending a running sandbox (e.g., ``"5m"``, ``"10m"``). Extensions cannot push the total beyond ``lifetime_max``.
+ * - ``lifetime_max``
+ - string
+ - No
+ - Maximum total runtime allowed for the sandbox (e.g., ``"20m"``, ``"60m"``). Once reached, the sandbox cannot be extended further.
+ * - ``policy``
+ - string
+ - Conditional
+ - Path or reference to the IAM policy that defines the sandbox's permissions. Controls access level from full to **least-privileged**.
+ * - ``provision_engine``
+ - string
+ - Conditional
+ - Infrastructure-as-Code engine used to orchestrate resources (e.g., ``terraform``). Include when the sandbox provisions resources.
+ * - ``billing_limit``
+ - number
+ - Optional
+ - Organizational cost control value associated with the sandbox.
+ * - ``parameters``
+ - mapping
+ - Conditional
+ - Type-specific inputs. Required for ``aws_ec2`` (see below).
+
+.. _ec2type-sandboxes:
+
+``settings.parameters`` for ``aws_ec2``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+After setting ``type`` to ``aws_ec2``, configure the following ``parameters``:
+
+.. list-table::
+ :widths: 22 14 10 54
+ :header-rows: 1
+
+ * - Key
+ - Type
+ - Required
+ - Description
+ * - ``instance_type``
+ - string
+ - Yes
+ - EC2 instance type to provision (e.g., ``t3.medium``).
+ * - ``image``
+ - string
+ - Yes
+ - AMI name or ID for the VM image (e.g., ``codio-aws-windows-base``).
+ * - ``volume_size``
+ - integer (GB)
+ - Yes
+ - Root volume size in GiB for the instance (e.g., ``30``).
+ * - ``connection_mode``
+ - enum
+ - Optional
+ - Default access channel for the instance. Supported values in examples: ``ssh`` or ``vnc``.
+
+Permissions (Full to Least-Privileged)
+--------------------------------------
+
+Sandbox **permissions are chosen by you** via the attached ``policy``:
+
+- To grant **broad administrative capabilities**, point ``policy`` to an IAM policy that allows those actions.
+- To enforce **least-privileged** access, supply a narrowly scoped policy granting only the minimum actions required.
+
+This design lets you run anything from **full AWS** environments to tightly constrained labs, all with the same sandbox mechanism.
+
+.. admonition:: How can I create a policy?
+
+ Policies are an AWS concept. If you want to read more, please visit AWS' guide to
+ `Policies and permissions`__ and the
+ `IAM tutorial: Delegate access across AWS accounts using IAM roles`__.
+
+ __ https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html
+ __ https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html
+
diff --git a/source/instructors/getstarted/support/changelog.rst b/source/instructors/getstarted/support/changelog.rst
index 941e770e..98b46dc0 100644
--- a/source/instructors/getstarted/support/changelog.rst
+++ b/source/instructors/getstarted/support/changelog.rst
@@ -13,6 +13,10 @@ We do not report small bug fixes or issues that affect individual users, who we
`[I]` = Update or improvement
`[F]` = New feature
+**November 2025**
+
+- `[I] [10th]`: Added the option to launch :ref:`EC2 VMs using Sandboxes
`.
+
**October 2025**
- `[I] [24th]`: Pyret support has been deprecated.
@@ -29,6 +33,7 @@ We do not report small bug fixes or issues that affect individual users, who we
**August 2025**
+- `[F] [26th]`: Added :ref:`sandboxes` for teaching AWS Cloud.
- `[I] [21st]`: The Join a Course token is now four words instead of two.
**July 2025**
diff --git a/student-source/_static/css/custom_body.css b/student-source/_static/css/custom_body.css
index 456eaea6..067e3fdc 100644
--- a/student-source/_static/css/custom_body.css
+++ b/student-source/_static/css/custom_body.css
@@ -1,174 +1,111 @@
/* =========================================
- Codio Docs — Home (index) only
- Pure theme background, centered layout
+ Codio Docs — Landing pages (Instructor & Student)
+ Layout resets + shared design tokens
========================================= */
-/* Hide sidebars on the homepage and center content */
-/* Center & unbox the student page just like the home page */
-.student-page .bd-sidebar-primary,
-.student-page .bd-sidebar-secondary { display: none !important; }
+/* Hide the sidebars and unbox the layout on the landing pages */
+.landing-page .bd-sidebar-primary,
+.landing-page .bd-sidebar-secondary {
+ display: none !important;
+}
-.student-page .bd-content,
-.student-page .bd-article-container {
+.landing-page .bd-content,
+.landing-page .bd-article-container {
max-width: 1200px;
margin-inline: auto !important;
- padding-inline: 1rem;
+ padding-inline: clamp(1rem, 5vw, 2.5rem);
background: transparent !important;
}
-/* Share the same theme tokens */
-html[data-theme="dark"] .student-page,
-[data-theme="dark"] .student-page {
- --text: #e6eaf2;
- --muted: #9aa3b2;
- --border: rgba(255,255,255,0.14);
- --surface-hover: rgba(255,255,255,0.06);
- --brand: #6ee7ff;
- --brand-strong: #3dd8ff;
- --ring: #9feaff;
- --shadow: 0 10px 26px rgba(3,8,48,0.35);
-}
-html[data-theme="light"] .student-page,
-[data-theme="light"] .student-page {
- --text: #0b1020;
- --muted: #475569;
- --border: rgba(0,0,0,0.12);
- --surface-hover: rgba(2,132,199,0.05);
- --brand: #0ea5e9;
- --brand-strong: #0284c7;
- --ring: #38bdf8;
- --shadow: 0 10px 24px rgba(2,132,199,0.08);
-}
-
-.index-page .bd-sidebar-primary,
-.index-page .bd-sidebar-secondary { display: none !important; }
-
-.index-page .bd-content,
-.index-page .bd-article-container {
- max-width: 100%;
- margin-inline: auto !important;
- padding-inline: 1rem;
-}
-
-/* Make every wrapper transparent so there is NO boxed panel */
-.index-page,
-.index-page .bd-main,
-.index-page .bd-content,
-.index-page .bd-article-container,
-.index-page .bd-article {
+.landing-page,
+.landing-page .bd-main,
+.landing-page .bd-content,
+.landing-page .bd-article,
+.landing-page .bd-article-container {
background: transparent !important;
}
-/* ------- Theme tokens (only for the homepage components) ------- */
-html[data-theme="dark"] .index-page,
-[data-theme="dark"] .index-page {
- --text: #e6eaf2;
- --muted: #9aa3b2;
- --border: rgba(255,255,255,0.14);
- --surface-hover: rgba(255,255,255,0.06);
- --brand: #6ee7ff;
- --brand-strong: #3dd8ff;
- --ring: #9feaff;
- --shadow: 0 10px 26px rgba(3,8,48,0.35);
+.landing-page {
+ color-scheme: light dark;
+ color: var(--landing-text);
+ --landing-text: #0f172a;
+ --landing-muted: #475569;
+ --landing-border: rgba(15, 23, 42, 0.12);
+ --landing-surface: rgba(255, 255, 255, 0.85);
+ --landing-surface-hover: rgba(14, 165, 233, 0.08);
+ --landing-accent: #0284c7;
+ --landing-accent-strong: #0369a1;
+ --landing-ring: rgba(14, 165, 233, 0.45);
+ --landing-card-shadow: 0 16px 28px rgba(15, 23, 42, 0.12);
+ --landing-card-shadow-dark: 0 22px 40px rgba(8, 47, 73, 0.45);
+ --landing-button-shadow: 0 14px 28px rgba(2, 132, 199, 0.18);
+ --landing-icon-shadow: 0 14px 28px rgba(2, 132, 199, 0.24);
+ --landing-hero-shadow: 0 24px 48px rgba(15, 118, 210, 0.12);
+ --landing-hero-from: #eff6ff;
+ --landing-hero-to: #dbeafe;
}
-html[data-theme="light"] .index-page,
-[data-theme="light"] .index-page {
- --text: #0b1020;
- --muted: #475569;
- --border: rgba(0,0,0,0.12);
- --surface-hover: rgba(2,132,199,0.05);
- --brand: #0ea5e9;
- --brand-strong: #0284c7;
- --ring: #38bdf8;
- --shadow: 0 10px 24px rgba(2,132,199,0.08);
+html[data-theme="dark"] .landing-page,
+[data-theme="dark"] .landing-page {
+ --landing-text: #e5edff;
+ --landing-muted: #94a3b8;
+ --landing-border: rgba(148, 163, 184, 0.18);
+ --landing-surface: rgba(15, 23, 42, 0.68);
+ --landing-surface-hover: rgba(56, 189, 248, 0.18);
+ --landing-accent: #38bdf8;
+ --landing-accent-strong: #0ea5e9;
+ --landing-ring: rgba(56, 189, 248, 0.6);
+ --landing-card-shadow: var(--landing-card-shadow-dark);
+ --landing-button-shadow: 0 18px 36px rgba(15, 118, 210, 0.36);
+ --landing-icon-shadow: 0 20px 38px rgba(14, 165, 233, 0.45);
+ --landing-hero-shadow: 0 26px 52px rgba(2, 132, 199, 0.32);
+ --landing-hero-from: #0f172a;
+ --landing-hero-to: #111827;
}
-/* Fallback to OS pref if the site doesn't set data-theme */
-@media (prefers-color-scheme: light) {
- html:not([data-theme="dark"]) .index-page {
- --text: #0b1020;
- --muted: #475569;
- --border: rgba(0,0,0,0.12);
- --surface-hover: rgba(2,132,199,0.05);
- --brand: #0ea5e9;
- --brand-strong: #0284c7;
- --ring: #38bdf8;
- --shadow: 0 10px 24px rgba(2,132,199,0.08);
+@media (prefers-color-scheme: dark) {
+ html:not([data-theme="light"]) .landing-page {
+ --landing-text: #e5edff;
+ --landing-muted: #94a3b8;
+ --landing-border: rgba(148, 163, 184, 0.18);
+ --landing-surface: rgba(15, 23, 42, 0.68);
+ --landing-surface-hover: rgba(56, 189, 248, 0.18);
+ --landing-accent: #38bdf8;
+ --landing-accent-strong: #0ea5e9;
+ --landing-ring: rgba(56, 189, 248, 0.6);
+ --landing-card-shadow: var(--landing-card-shadow-dark);
+ --landing-button-shadow: 0 18px 36px rgba(15, 118, 210, 0.36);
+ --landing-icon-shadow: 0 20px 38px rgba(14, 165, 233, 0.45);
+ --landing-hero-shadow: 0 26px 52px rgba(2, 132, 199, 0.32);
+ --landing-hero-from: #0f172a;
+ --landing-hero-to: #111827;
}
}
-/* ------- Hero ------- */
-.home-hero { padding: 5rem 1rem 1.5rem; text-align: center; }
-.home-title { color: var(--text); font-size: clamp(2rem, 2.8vw + 1rem, 3rem); margin: 0 0 .6rem; }
-.home-subtitle { color: var(--muted); margin: 0 auto 1.8rem; max-width: 62ch; }
-
-/* (Search form is commented out in your HTML, but styles left here in case you re-enable) */
-.home-search {
- display: inline-flex; gap: .5rem; align-items: center;
- background: transparent; /* no panel */
- border: 1px solid var(--border);
- border-radius: 999px; padding: .5rem;
- box-shadow: none;
-}
-.home-search input[type="search"] {
- min-width: clamp(260px, 42vw, 560px);
- background: transparent; border: none; color: var(--text);
- outline: none; padding: .75rem 1rem; font-size: 1rem;
-}
-.home-search input::placeholder { color: var(--muted); }
-.search-btn {
- display: inline-flex; align-items: center; gap: .5rem;
- border: 1px solid transparent;
- background: linear-gradient(180deg, var(--brand), var(--brand-strong));
- color: #001; font-weight: 700; padding: .7rem 1rem;
- border-radius: 999px; cursor: pointer;
- box-shadow: 0 10px 24px rgba(0, 190, 255, .18);
-}
-.search-btn:focus-visible { outline: 3px solid var(--ring); outline-offset: 2px; }
-.search-btn .icon { width: 20px; height: 20px; }
-
-/* ------- Sections ------- */
-.home-main { padding: .5rem 0 3rem; }
-.home-container { max-width: 1100px; margin-inline: auto; padding-inline: 1rem; }
-.section { margin-top: 1.25rem; }
-.section-title { color: var(--muted); font-weight: 600; letter-spacing: .2px; margin: 0 0 1rem; }
-
-/* ------- Cards: transparent by default (no fill), subtle outline ------- */
-.card-grid {
- --min: 260px;
- display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--min), 1fr));
- gap: 1rem; align-items: stretch;
-}
-.card {
- display: grid; grid-template-rows: auto auto 1fr auto; gap: .5rem;
- padding: 1rem; border-radius: 16px; text-decoration: none; color: var(--text);
- background: transparent; /* <- no box */
- border: 1px solid var(--border);
- box-shadow: none;
- transition: transform .2s ease, background-color .15s ease, border-color .15s ease;
-}
-.card:hover { transform: translateY(-2px); background: var(--surface-hover); border-color: var(--brand); }
-.card:focus-visible { outline: 3px solid var(--ring); outline-offset: 2px; }
-
-.card-icon {
- width: 36px; height: 36px; display: grid; place-items: center;
- border-radius: 12px;
- background: linear-gradient(180deg, var(--brand), var(--brand-strong));
- box-shadow: 0 8px 18px rgba(0, 190, 255, .2);
+@media (prefers-color-scheme: light) {
+ html:not([data-theme="dark"]) .landing-page {
+ --landing-text: #0f172a;
+ --landing-muted: #475569;
+ --landing-border: rgba(15, 23, 42, 0.12);
+ --landing-surface: rgba(255, 255, 255, 0.85);
+ --landing-surface-hover: rgba(14, 165, 233, 0.08);
+ --landing-accent: #0284c7;
+ --landing-accent-strong: #0369a1;
+ --landing-ring: rgba(14, 165, 233, 0.45);
+ --landing-card-shadow: 0 16px 28px rgba(15, 23, 42, 0.12);
+ --landing-button-shadow: 0 14px 28px rgba(2, 132, 199, 0.18);
+ --landing-icon-shadow: 0 14px 28px rgba(2, 132, 199, 0.24);
+ --landing-hero-shadow: 0 24px 48px rgba(15, 118, 210, 0.12);
+ --landing-hero-from: #eff6ff;
+ --landing-hero-to: #dbeafe;
+ }
}
-.card-icon svg { width: 22px; height: 22px; fill: #001; }
-
-.card-title { font-size: 1.05rem; margin: .25rem 0; }
-.card-desc { margin: 0; color: var(--muted); }
-.card-cta { margin-top: .5rem; font-weight: 700; color: var(--brand); display: inline-flex; gap: .35rem; }
-.card-cta::after { content: "›"; font-size: 1.2em; transform: translateY(-1px); }
-/* Reduced motion */
-@media (prefers-reduced-motion: reduce) { .card, .search-btn { transition: none; } }
+.section { scroll-margin-top: 5rem; }
-/* Small screens */
-@media (max-width: 480px) {
- .home-search { width: 100%; }
- .home-search input[type="search"] { min-width: 0; width: 100%; }
+/* Retain backwards-compat class hooks */
+.index-page .home-container,
+.student-page .home-container {
+ max-width: 1100px;
+ margin-inline: auto;
}
diff --git a/student-source/_static/css/custom_index.css b/student-source/_static/css/custom_index.css
index 729bfd77..3492c794 100644
--- a/student-source/_static/css/custom_index.css
+++ b/student-source/_static/css/custom_index.css
@@ -1,12 +1,160 @@
-/* Slight width nudge only (kept minimal) */
-.index-page .home-container { max-width: 100%; }
-
-/* Ensure any Bootstrap-like main column stays centered on the homepage */
-@media (min-width: 1200px) {
- .index-page .col-xl-8 {
- flex: 0 0 91.66667% !important;
- max-width: 91.66667% !important;
- margin-left: auto !important;
- margin-right: auto !important;
- }
+/* =========================================
+ Shared landing page components
+ Hero, cards, and responsive utilities
+ ========================================= */
+
+.home-container { padding-inline: clamp(1.5rem, 5vw, 3rem); }
+
+.landing-hero {
+ padding: clamp(2rem, 4vw, 5.25rem) clamp(1.5rem, 4vw, 2.5rem);
+ background: linear-gradient(135deg, var(--landing-hero-from), var(--landing-hero-to));
+ border-radius: clamp(24px, 5vw, 40px);
+ position: relative;
+ overflow: hidden;
+ box-shadow: var(--landing-hero-shadow);
+ margin-inline: clamp(1rem, 4vw, 2.5rem);
+}
+
+.hero-inner { position: relative; text-align: center; max-width: 52rem; margin-inline: auto; }
+.hero-eyebrow {
+ font-size: .8rem;
+ letter-spacing: .32em;
+ text-transform: uppercase;
+ font-weight: 600;
+ color: var(--landing-muted);
+ margin-bottom: 0.85rem;
+}
+.hero-title { font-size: clamp(2.3rem, 2.4vw + 1.5rem, 3.4rem); line-height: 1.1; margin: 0 0 1.15rem; color: var(--landing-text); }
+.hero-lede { color: var(--landing-muted); font-size: clamp(1.05rem, 1vw + .9rem, 1.25rem); margin: 0 auto clamp(1.6rem, 3.5vw, 2.3rem); max-width: 62ch; }
+
+.hero-actions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: .85rem;
+}
+
+.hero-button,
+.hero-link {
+ display: inline-flex;
+ align-items: center;
+ gap: .5rem;
+ border-radius: 999px;
+ font-weight: 600;
+ text-decoration: none;
+ transition: transform .2s ease, box-shadow .2s ease, background-color .2s ease, border-color .2s ease;
+}
+
+.hero-button {
+ padding: .85rem 1.5rem;
+ background: linear-gradient(135deg, var(--landing-accent), var(--landing-accent-strong));
+ color: #f8fafc;
+ box-shadow: var(--landing-button-shadow);
+}
+
+.hero-link {
+ padding: .82rem 1.3rem;
+ background: transparent;
+ border: 1px solid var(--landing-border);
+ color: var(--landing-text);
+}
+
+.hero-button:hover { transform: translateY(-1px); box-shadow: 0 18px 36px rgba(14, 165, 233, .22); }
+.hero-link:hover { transform: translateY(-1px); border-color: var(--landing-accent); color: var(--landing-accent); }
+
+.hero-button:focus-visible,
+.hero-link:focus-visible,
+.card:focus-visible {
+ outline: 3px solid var(--landing-ring);
+ outline-offset: 3px;
+}
+
+.home-main { padding-block: clamp(2.5rem, 4vw, 4rem); }
+.section { margin-top: clamp(1.5rem, 2vw, 2.5rem); }
+.section-title { font-size: .95rem; text-transform: uppercase; letter-spacing: .28em; color: var(--landing-muted); margin-bottom: 1.5rem; text-align: center; }
+
+.card-grid {
+ --card-min: 260px;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(var(--card-min), 1fr));
+ gap: clamp(1rem, 2vw, 1.8rem);
+ align-items: stretch;
+}
+
+.card-grid.cards-4 {
+ --card-min: 280px;
+ max-width: min(880px, 100%);
+ margin-inline: auto;
+}
+
+.card-grid.cards-6 { --card-min: 220px; }
+
+.card {
+ position: relative;
+ display: grid;
+ grid-template-rows: auto auto 1fr auto;
+ gap: .65rem;
+ padding: clamp(1.25rem, 2vw, 1.75rem);
+ border-radius: 20px;
+ text-decoration: none;
+ color: var(--landing-text);
+ background: var(--landing-surface);
+ border: 1px solid color-mix(in srgb, var(--landing-border) 70%, transparent);
+ box-shadow: var(--landing-card-shadow);
+ backdrop-filter: blur(6px);
+ transition: transform .22s ease, background-color .18s ease, border-color .18s ease, box-shadow .2s ease;
+}
+
+.card:hover {
+ transform: translateY(-4px);
+ border-color: color-mix(in srgb, var(--landing-accent) 55%, transparent);
+ box-shadow: 0 24px 44px rgba(14, 165, 233, 0.22);
+}
+
+.card-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 14px;
+ display: grid;
+ place-items: center;
+ background: linear-gradient(135deg, var(--landing-accent), var(--landing-accent-strong));
+ box-shadow: var(--landing-icon-shadow);
+}
+
+.card-icon svg { width: 24px; height: 24px; fill: #f8fafc; }
+
+.card-title { font-size: 1.15rem; margin: 0; }
+.card-desc { margin: 0; color: var(--landing-muted); line-height: 1.55; }
+.card-cta {
+ display: inline-flex;
+ align-items: center;
+ gap: .4rem;
+ margin-top: .2rem;
+ font-weight: 600;
+ color: var(--landing-accent);
+}
+
+.card-cta::after { content: "→"; font-size: 1.1em; transition: transform .18s ease; }
+.card:hover .card-cta::after { transform: translateX(3px); }
+
+/* Motion & accessibility */
+@media (prefers-reduced-motion: reduce) {
+ .hero-button,
+ .hero-link,
+ .card { transition: none; }
+ .card:hover .card-cta::after { transform: none; }
+}
+
+/* Responsive tweaks */
+@media (max-width: 720px) {
+ .landing-hero { border-radius: clamp(18px, 8vw, 30px); }
+ .hero-actions { flex-direction: column; }
+ .hero-link, .hero-button { width: 100%; justify-content: center; }
+ .section-title { text-align: left; }
+}
+
+@media (max-width: 520px) {
+ .home-container { padding-inline: clamp(1rem, 6vw, 1.5rem); }
+ .landing-hero { padding-inline: clamp(1.2rem, 5vw, 1.8rem); margin-inline: 0; }
+ .card-grid { grid-template-columns: 1fr; }
}
diff --git a/student-source/_templates/layout.html b/student-source/_templates/layout.html
new file mode 100644
index 00000000..7ecb168e
--- /dev/null
+++ b/student-source/_templates/layout.html
@@ -0,0 +1,38 @@
+{% extends "!layout.html" %}
+
+{% block extrahead %}
+ {{ super() }}
+
+{% endblock %}
+
+{% block footer %}
+{{ super() }}
+
+
+
+
+
+
+{% endblock %}
diff --git a/student-source/_templates/student.html b/student-source/_templates/student.html
index 772b17e0..d00fec77 100644
--- a/student-source/_templates/student.html
+++ b/student-source/_templates/student.html
@@ -4,20 +4,21 @@
-