// ============================================================
// phone_mount_bracket.scad — Spring-Loaded Phone Mount for T-Slot Rail
// Issue: #535  Agent: sl-mechanical  Date: 2026-03-07
// ============================================================
//
// Parametric spring-loaded phone mount that clamps to the 2020 aluminium
// T-slot sensor rail.  Adjustable phone width 60–85 mm.  Quick-release
// cam lever for tool-free phone swap.  Vibration-damping flexure ribs
// on grip pads absorb motor/terrain vibration (PETG compliance).
//
// Design overview:
//   - Fixed jaw + sliding jaw on a 40 mm guide rail (M4 rod)
//   - Coil spring (Ø8 × 30 mm) compressed between jaw and end-stop —
//     spring pre-load keeps phone clamped at any width in range
//   - Cam lever (printed PETG) rotates 90° to release / lock spring
//   - Anti-vibration flexure ribs on both grip pad faces
//   - Landscape or portrait orientation: bracket rotates on T-nut base
//
// Parts (STL exports):
//   Part 1 — tnut_base()          Rail attachment base (universal)
//   Part 2 — fixed_jaw()          Fixed bottom jaw + guide rail bosses
//   Part 3 — sliding_jaw()        Spring-loaded sliding jaw
//   Part 4 — cam_lever()          Quick-release cam lever
//   Part 5 — grip_pad()           Flexure grip pad (print ×2, TPU optional)
//   Part 6 — assembly_preview()   Full assembly
//
// Hardware BOM (per mount):
//   1× M4 × 60 mm SHCS          guide rod + spring bolt
//   1× M4 hex nut                end-stop on sliding jaw
//   1× Ø8 × 30 mm coil spring   ~0.5 N/mm rate (spring clamping)
//   2× M3 × 16 mm SHCS          T-nut base thumbscrew + arm bolts
//   1× M3 hex nut                thumbscrew nut in T-nut
//   4× M2 × 8 mm SHCS           grip pad retention bolts (optional)
//
// Dimensions:
//   Phone width range : PHONE_W_MIN–PHONE_W_MAX (60–85 mm) parametric
//   Phone thickness   : up to PHONE_THICK_MAX (12 mm) — open-front jaw
//   Phone height held : GRIP_SPAN (22 mm each jaw) — portrait/landscape
//   Overall bracket H : ~110 mm  W: ~90 mm  D: ~55 mm
//
// Print settings:
//   Material  : PETG (tnut_base, fixed_jaw, sliding_jaw, cam_lever)
//               TPU 95A optional for grip_pad (or PETG for rigidity)
//   Perimeters: 5 (structural parts), 3 (grip_pad)
//   Infill    : 40 % gyroid (jaws), 20 % (grip_pad)
//   Supports  : none needed (designed for FDM orientation)
//   Layer ht  : 0.2 mm
//
// Export commands:
//   openscad phone_mount_bracket.scad -D 'RENDER="tnut_base_stl"'    -o pm_tnut_base.stl
//   openscad phone_mount_bracket.scad -D 'RENDER="fixed_jaw_stl"'    -o pm_fixed_jaw.stl
//   openscad phone_mount_bracket.scad -D 'RENDER="sliding_jaw_stl"'  -o pm_sliding_jaw.stl
//   openscad phone_mount_bracket.scad -D 'RENDER="cam_lever_stl"'    -o pm_cam_lever.stl
//   openscad phone_mount_bracket.scad -D 'RENDER="grip_pad_stl"'     -o pm_grip_pad.stl
// ============================================================

$fn = 64;
e   = 0.01;   // epsilon for boolean clearance

// ── Phone parameters (adjust to target device) ───────────────────────────────
PHONE_W_MIN    = 60.0;   // narrowest phone width supported (mm)
PHONE_W_MAX    = 85.0;   // widest phone width supported (mm)
PHONE_THICK_MAX = 12.0;  // max phone body thickness incl. case (mm)

// ── Rail geometry (must match sensor_rail.scad) ──────────────────────────────
RAIL_W         = 20.0;
SLOT_OPEN      =  6.0;
SLOT_INNER_W   = 10.2;
SLOT_INNER_H   =  5.8;
SLOT_NECK_H    =  3.2;

// ── T-nut constants ───────────────────────────────────────────────────────────
TNUT_W         =  9.8;
TNUT_H         =  5.5;
TNUT_L         = 12.0;
TNUT_M3_NUT_AF =  5.5;
TNUT_M3_NUT_H  =  2.5;
TNUT_BOLT_D    =  3.3;   // M3 clearance

// ── Base plate geometry ───────────────────────────────────────────────────────
BASE_FACE_W  = 30.0;
BASE_FACE_H  = 25.0;
BASE_FACE_T  = SLOT_NECK_H + 1.5;

// ── Jaw geometry ─────────────────────────────────────────────────────────────
JAW_BODY_W   = 88.0;    // jaw outer width (> PHONE_W_MAX for rim)
JAW_BODY_H   = 28.0;    // jaw height (Z) — phone grip span
JAW_BODY_T   = 14.0;    // jaw depth (Y) — phone cradled this deep
JAW_WALL_T   =  4.0;    // jaw side wall thickness
JAW_LIP_T    =  3.0;    // front retaining lip thickness
JAW_LIP_H    =  5.0;    // front lip height (retains phone)
PHONE_POCKET_D = PHONE_THICK_MAX + 0.5;  // pocket depth for phone

// ── Guide rod / spring system ─────────────────────────────────────────────────
GUIDE_ROD_D  =  4.3;    // M4 clearance bore in sliding jaw
GUIDE_BOSS_D = 10.0;    // boss OD around guide bore
GUIDE_BOSS_T =  6.0;    // boss length
SPRING_OD    =  8.5;    // coil spring OD pocket (spring is Ø8)
SPRING_L     = 32.0;    // spring pocket length (spring compressed ~22 mm)
SPRING_SEAT_T =  3.0;   // spring seat wall at end-stop boss
JAW_TRAVEL   = PHONE_W_MAX - PHONE_W_MIN + 4.0;   // max jaw travel (mm)
ARM_SPAN     = PHONE_W_MAX + 2 * JAW_WALL_T + 8;  // fixed jaw total width

// ── Cam lever geometry ────────────────────────────────────────────────────────
CAM_R_MIN    =  5.0;    // cam small radius (engaged / clamped)
CAM_R_MAX    =  9.0;    // cam large radius (released, spring compressed)
CAM_THICK    =  8.0;    // cam disc thickness
CAM_HANDLE_L = 45.0;    // lever arm length
CAM_HANDLE_W =  8.0;    // lever handle width
CAM_HANDLE_T =  5.0;    // lever handle thickness
CAM_BORE_D   =  4.3;    // M4 pivot bore
CAM_DETENT_D =  3.0;    // detent ball pocket (3 mm bearing)

// ── Grip pad geometry (vibration dampening flexure ribs) ─────────────────────
PAD_W        = JAW_BODY_W - 2*JAW_WALL_T - 2;  // pad width
PAD_H        = JAW_BODY_H - 2;                  // pad height
PAD_T        =  2.5;    // pad body thickness
RIB_H        =  1.5;    // flexure rib height above pad face
RIB_W        =  1.2;    // rib width
RIB_PITCH    =  5.0;    // rib pitch (centre-to-centre)
RIB_COUNT    = floor(PAD_W / RIB_PITCH) - 1;

// ── Arm geometry (base to jaw body) ──────────────────────────────────────────
ARM_REACH    = 38.0;    // distance from rail face to jaw centreline (+Y)
ARM_T        =  4.0;    // arm thickness
ARM_H        = BASE_FACE_H;

// ── Fasteners ─────────────────────────────────────────────────────────────────
M2_D = 2.4;
M3_D = 3.3;
M4_D = 4.3;
M4_NUT_AF = 7.0;   // M4 hex nut across-flats
M4_NUT_H  = 3.2;   // M4 hex nut height

// ============================================================
// RENDER DISPATCH
// ============================================================
RENDER = "assembly";

if      (RENDER == "assembly")        assembly_preview();
else if (RENDER == "tnut_base_stl")   tnut_base();
else if (RENDER == "fixed_jaw_stl")   fixed_jaw();
else if (RENDER == "sliding_jaw_stl") sliding_jaw();
else if (RENDER == "cam_lever_stl")   cam_lever();
else if (RENDER == "grip_pad_stl")    grip_pad();

// ============================================================
// ASSEMBLY PREVIEW
// ============================================================
module assembly_preview() {
    // Ghost rail section (20 × 20 × 200)
    %color("Silver", 0.30)
        linear_extrude(200)
            square([RAIL_W, RAIL_W], center = true);

    // T-nut base at Z=80 on rail
    color("OliveDrab", 0.85)
        translate([0, 0, 80])
            tnut_base();

    // Fixed jaw assembly (centred, extending +Y from base)
    color("DarkSlateGray", 0.85)
        translate([0, SLOT_NECK_H + BASE_FACE_T + ARM_REACH, 80])
            fixed_jaw();

    // Sliding jaw — shown at mid-travel (phone ~72 mm wide)
    color("SteelBlue", 0.85)
        translate([PHONE_W_MIN + (PHONE_W_MAX - PHONE_W_MIN)/2,
                   SLOT_NECK_H + BASE_FACE_T + ARM_REACH, 80])
            sliding_jaw();

    // Grip pads on both jaws
    color("DimGray", 0.85) {
        translate([0, SLOT_NECK_H + BASE_FACE_T + ARM_REACH, 80])
            translate([JAW_WALL_T, JAW_BODY_T, JAW_BODY_H/2])
                rotate([90, 0, 0])
                    grip_pad();
        translate([PHONE_W_MIN + (PHONE_W_MAX - PHONE_W_MIN)/2,
                   SLOT_NECK_H + BASE_FACE_T + ARM_REACH, 80])
            translate([-JAW_WALL_T - PAD_T, JAW_BODY_T, JAW_BODY_H/2])
                rotate([90, 0, 180])
                    grip_pad();
    }

    // Cam lever — shown in locked (clamped) position
    color("OrangeRed", 0.85)
        translate([ARM_SPAN/2 + 6,
                   SLOT_NECK_H + BASE_FACE_T + ARM_REACH + GUIDE_BOSS_D/2,
                   80 + JAW_BODY_H/2])
            rotate([0, 0, 0])
                cam_lever();
}

// ============================================================
// PART 1 — T-NUT BASE
// ============================================================
// Standard 2020 T-slot rail attachment base.
// Identical interface to sensor_rail_brackets.scad universal_tnut_base().
// Arm extends in +Y; rail clamp bolt in -Y face.
//
// Print flat (face plate down), PETG, 5 perims, 60 % infill.
module tnut_base() {
    difference() {
        union() {
            // Face plate (flush against rail outer face)
            translate([-BASE_FACE_W/2, -BASE_FACE_T, 0])
                cube([BASE_FACE_W, BASE_FACE_T, BASE_FACE_H]);

            // T-nut neck (enters rail slot)
            translate([-TNUT_W/2, 0, (BASE_FACE_H - TNUT_L)/2])
                cube([TNUT_W, SLOT_NECK_H + e, TNUT_L]);

            // T-nut body (wider, inside T-groove)
            translate([-TNUT_W/2, SLOT_NECK_H - e, (BASE_FACE_H - TNUT_L)/2])
                cube([TNUT_W, TNUT_H - SLOT_NECK_H + e, TNUT_L]);

            // Arm stub (face plate → jaw)
            translate([-BASE_FACE_W/2, -BASE_FACE_T, 0])
                cube([BASE_FACE_W, BASE_FACE_T + ARM_REACH, ARM_T]);
        }

        // M3 rail clamp bolt bore (centre of T-nut, through face plate)
        translate([0, -BASE_FACE_T - e, BASE_FACE_H/2])
            rotate([-90, 0, 0])
                cylinder(d = TNUT_BOLT_D, h = BASE_FACE_T + TNUT_H + 2*e);

        // M3 hex nut pocket (inside T-nut body)
        translate([0, SLOT_NECK_H + 0.3, BASE_FACE_H/2])
            rotate([-90, 0, 0])
                cylinder(d = TNUT_M3_NUT_AF / cos(30),
                         h = TNUT_M3_NUT_H + 0.3,
                         $fn = 6);

        // 2× M3 bolt holes for arm-to-jaw bolting
        for (bx = [-10, 10])
            translate([bx, ARM_REACH - BASE_FACE_T - e, ARM_T/2])
                rotate([-90, 0, 0])
                    cylinder(d = M3_D, h = 8 + 2*e);

        // Lightening slot in arm
        translate([0, -BASE_FACE_T/2 + ARM_REACH/2, ARM_T/2])
            cube([BASE_FACE_W - 12, ARM_REACH - 16, ARM_T + 2*e],
                 center = true);
    }
}

// ============================================================
// PART 2 — FIXED JAW
// ============================================================
// Fixed lower jaw of the clamping system.  Phone sits in the pocket
// formed by the fixed jaw (bottom) and sliding jaw (top).
// Two guide bosses on the right wall carry the M4 guide rod + spring.
// The cam lever pivot boss is on the outer right face.
//
// Coordinate origin: centre-bottom of jaw body.
// Phone entry face: +Y (open front), phone pocket opens toward +Y.
// Fixed jaw left edge is at X = -JAW_BODY_W/2.
//
// Print jaw-pocket-face down, PETG, 5 perims, 40 % infill.
module fixed_jaw() {
    difference() {
        union() {
            // ── Main jaw body ────────────────────────────────────────────
            translate([-JAW_BODY_W/2, -JAW_BODY_T/2, 0])
                cube([JAW_BODY_W, JAW_BODY_T, JAW_BODY_H]);

            // ── Front retaining lip (keeps phone from falling forward) ───
            translate([-JAW_BODY_W/2, JAW_BODY_T/2 - JAW_LIP_T, 0])
                cube([JAW_BODY_W, JAW_LIP_T, JAW_LIP_H]);

            // ── Guide boss right (outer, carries spring + end-stop) ──────
            translate([JAW_BODY_W/2, 0, JAW_BODY_H/2])
                rotate([0, 90, 0])
                    cylinder(d = GUIDE_BOSS_D, h = GUIDE_BOSS_T);

            // ── Cam lever pivot boss (right face, above guide boss) ──────
            translate([JAW_BODY_W/2, 0, JAW_BODY_H/2 + GUIDE_BOSS_D + 4])
                rotate([0, 90, 0])
                    cylinder(d = CAM_THICK + 4, h = 6);

            // ── Arm attachment bosses (left side, connect to tnut_base) ──
            for (bx = [-10, 10])
                translate([bx, -JAW_BODY_T/2 - 8, ARM_T/2])
                    cylinder(d = 8, h = 8);
        }

        // ── Phone pocket (open-top U channel centred in jaw) ────────────
        // Pocket opens toward +Y (front), phone drops in from above.
        translate([0, -JAW_BODY_T/2 - e,
                   JAW_LIP_H])
            cube([JAW_BODY_W - 2*JAW_WALL_T,
                  PHONE_POCKET_D + JAW_WALL_T,
                  JAW_BODY_H - JAW_LIP_H + e],
                 center = [true, false, false]);

        // ── Guide rod bore (M4 clearance, through both guide bosses) ────
        translate([-JAW_BODY_W/2 - e, 0, JAW_BODY_H/2])
            rotate([0, 90, 0])
                cylinder(d = GUIDE_ROD_D,
                         h = JAW_BODY_W + GUIDE_BOSS_T + 2*e);

        // ── Spring pocket (coaxial with guide rod, in right boss) ────────
        translate([JAW_BODY_W/2 + e, 0, JAW_BODY_H/2])
            rotate([0, -90, 0])
                cylinder(d = SPRING_OD, h = SPRING_L);

        // ── M4 hex nut pocket in spring-seat wall (end-stop nut) ────────
        translate([JAW_BODY_W/2 + GUIDE_BOSS_T + e, 0, JAW_BODY_H/2])
            rotate([0, -90, 0])
                cylinder(d = M4_NUT_AF / cos(30), h = M4_NUT_H + 0.5,
                         $fn = 6);

        // ── Cam pivot bore (M4 pivot, through pivot boss) ────────────────
        translate([JAW_BODY_W/2 - e, 0, JAW_BODY_H/2 + GUIDE_BOSS_D + 4])
            rotate([0, 90, 0])
                cylinder(d = CAM_BORE_D, h = 6 + 2*e);

        // ── Arm attachment bolt holes (M3, to tnut_base arm stubs) ──────
        for (bx = [-10, 10])
            translate([bx, -JAW_BODY_T/2 - 8 - e, ARM_T/2])
                rotate([-90, 0, 0])
                    cylinder(d = M3_D, h = 12 + 2*e);

        // ── Grip pad seats (recessed Ø1.5 mm, 2 mm deep, optional) ──────
        for (pz = [JAW_BODY_H * 0.3, JAW_BODY_H * 0.7])
        for (px = [-PAD_W/4, PAD_W/4])
            translate([px, -JAW_BODY_T/2 + PHONE_POCKET_D + 1, pz])
                rotate([-90, 0, 0])
                    cylinder(d = M2_D, h = 10);

        // ── Lightening pockets (non-structural core removal) ─────────────
        translate([0, 0, JAW_BODY_H/2])
            cube([JAW_BODY_W - 2*JAW_WALL_T - 4,
                  JAW_BODY_T - 2*JAW_WALL_T,
                  JAW_BODY_H - JAW_LIP_H - 4],
                 center = true);
    }
}

// ============================================================
// PART 3 — SLIDING JAW
// ============================================================
// Upper clamping jaw.  Slides along the M4 guide rod.
// Spring pushes this jaw toward the phone (inward).
// M4 hex nut on the guide rod limits maximum travel (full open).
// Cam lever pushes on this jaw face to compress spring (release).
//
// Coordinate origin same convention as fixed_jaw() for assembly.
// Jaw slides in +X direction (away from fixed jaw left wall).
//
// Print jaw-pocket-face down, PETG, 5 perims, 40 % infill.
module sliding_jaw() {
    difference() {
        union() {
            // ── Main jaw body ────────────────────────────────────────────
            translate([-JAW_WALL_T, -JAW_BODY_T/2, 0])
                cube([JAW_BODY_W/2 + JAW_WALL_T, JAW_BODY_T, JAW_BODY_H]);

            // ── Front retaining lip ──────────────────────────────────────
            translate([-JAW_WALL_T, JAW_BODY_T/2 - JAW_LIP_T, 0])
                cube([JAW_BODY_W/2 + JAW_WALL_T, JAW_LIP_T, JAW_LIP_H]);

            // ── Guide boss (carries guide rod, spring butts against face) ─
            translate([-JAW_WALL_T - GUIDE_BOSS_T, 0, JAW_BODY_H/2])
                rotate([0, 90, 0])
                    cylinder(d = GUIDE_BOSS_D, h = GUIDE_BOSS_T);

            // ── Cam follower ear (contacts cam lever) ────────────────────
            translate([-JAW_WALL_T - 2, 0,
                       JAW_BODY_H/2 + GUIDE_BOSS_D + 4])
                cube([4, CAM_THICK + 2, CAM_THICK + 2], center = true);
        }

        // ── Phone pocket (inner face, contacts phone side) ───────────────
        translate([-JAW_WALL_T - e, -JAW_BODY_T/2 - e, JAW_LIP_H])
            cube([JAW_BODY_W/2 - JAW_WALL_T + e,
                  PHONE_POCKET_D + JAW_WALL_T + 2*e,
                  JAW_BODY_H - JAW_LIP_H + e]);

        // ── Guide rod bore (M4 clearance through boss + jaw wall) ────────
        translate([-JAW_WALL_T - GUIDE_BOSS_T - e, 0, JAW_BODY_H/2])
            rotate([0, 90, 0])
                cylinder(d = GUIDE_ROD_D,
                         h = GUIDE_BOSS_T + JAW_WALL_T + 2*e);

        // ── M4 nut pocket (end-stop nut, rear of guide boss) ────────────
        translate([-JAW_WALL_T - GUIDE_BOSS_T - e, 0, JAW_BODY_H/2])
            rotate([0, 90, 0])
                cylinder(d = M4_NUT_AF / cos(30), h = M4_NUT_H + 1,
                         $fn = 6);

        // ── Cam follower bore (M4 pivot passes through ear) ─────────────
        translate([-JAW_WALL_T - 2 - e, 0,
                   JAW_BODY_H/2 + GUIDE_BOSS_D + 4])
            rotate([0, 90, 0])
                cylinder(d = CAM_BORE_D, h = 6 + 2*e);

        // ── Grip pad seats ───────────────────────────────────────────────
        for (pz = [JAW_BODY_H * 0.3, JAW_BODY_H * 0.7])
        for (px = [JAW_BODY_W/8])
            translate([px, -JAW_BODY_T/2 + PHONE_POCKET_D + 1, pz])
                rotate([-90, 0, 0])
                    cylinder(d = M2_D, h = 10);
    }
}

// ============================================================
// PART 4 — CAM LEVER (QUICK-RELEASE)
// ============================================================
// Eccentric cam disc + integral handle lever.
// Rotates 90° on M4 pivot pin between CLAMPED and RELEASED states:
//   CLAMPED  : cam small radius (CAM_R_MIN) toward jaw → spring pushes jaw
//   RELEASED : cam large radius (CAM_R_MAX) toward jaw → compresses spring
//               by (CAM_R_MAX - CAM_R_MIN) = 4 mm, opening jaw
//
// Detent ball pocket (Ø3 mm) snaps into rail-dimple for each position.
// Handle points rearward (-Y) in clamped state for low profile.
//
// Print standing on cam edge (cam disc vertical), PETG, 5 perims, 40%.
module cam_lever() {
    cam_offset = (CAM_R_MAX - CAM_R_MIN) / 2;   // 2 mm eccentricity
    union() {
        difference() {
            union() {
                // ── Eccentric cam disc ───────────────────────────────────
                // Offset so pivot bore is eccentric to disc profile
                translate([cam_offset, 0, 0])
                    cylinder(r = CAM_R_MAX, h = CAM_THICK, center = true);

                // ── Lever handle arm ─────────────────────────────────────
                hull() {
                    translate([cam_offset, 0, 0])
                        cylinder(r = CAM_R_MAX, h = CAM_THICK, center = true);
                    translate([cam_offset + CAM_HANDLE_L, 0, 0])
                        cylinder(r = CAM_HANDLE_W/2,
                                 h = CAM_HANDLE_T, center = true);
                }
            }

            // ── M4 pivot bore (through cam centre) ───────────────────────
            cylinder(d = CAM_BORE_D, h = CAM_THICK + 2*e, center = true);

            // ── Detent pockets (2× Ø3 mm, at 0° and 90°) ────────────────
            // Pocket at 0° → clamped detent
            translate([CAM_R_MAX - 2, 0, CAM_THICK/2 - 1.5])
                cylinder(d = CAM_DETENT_D + 0.2, h = 2);
            // Pocket at 90° → released detent
            translate([0, CAM_R_MAX - 2, CAM_THICK/2 - 1.5])
                cylinder(d = CAM_DETENT_D + 0.2, h = 2);

            // ── Lightening recesses on cam disc face ─────────────────────
            for (a = [0, 60, 120, 180, 240, 300])
                translate([cam_offset + (CAM_R_MAX - 4) * cos(a),
                           (CAM_R_MAX - 4) * sin(a), 0])
                    cylinder(d = 4, h = CAM_THICK + 2*e, center = true);

            // ── Handle grip grooves ──────────────────────────────────────
            for (i = [0:4])
                translate([cam_offset + 20 + i * 5, 0, 0])
                    rotate([90, 0, 0])
                        cylinder(d = 2.5, h = CAM_HANDLE_W + 2*e,
                                 center = true);
        }
    }
}

// ============================================================
// PART 5 — GRIP PAD (VIBRATION DAMPENING)
// ============================================================
// Flat pad with transverse flexure ribs that press against phone side.
// The rib profile (thin PETG fins) provides compliance in Z (vertical)
// absorbing vibration transmitted through the bracket.
// Optional: print in TPU 95A for superior damping.
// M2 bolts or adhesive-backed foam tape attach pad to jaw pocket face.
//
// Pad face (+Y) contacts phone.  Mounting face (-Y) bonds to jaw.
// Ribs run parallel to Z axis (vertical).
//
// Print flat (mounting face down), PETG or TPU 95A, 3 perims, 20%.
module grip_pad() {
    union() {
        // ── Base plate ───────────────────────────────────────────────────
        translate([-PAD_W/2, -PAD_T, -PAD_H/2])
            cube([PAD_W, PAD_T, PAD_H]);

        // ── Flexure ribs (transverse, dampening in Z) ────────────────────
        // RIB_COUNT ribs spaced RIB_PITCH apart, centred on pad
        for (i = [0 : RIB_COUNT - 1]) {
            rx = -PAD_W/2 + RIB_PITCH/2 + i * RIB_PITCH;
            if (abs(rx) <= PAD_W/2 - RIB_W/2)   // stay within pad
                translate([rx, 0, 0])
                    cube([RIB_W, RIB_H, PAD_H - 4], center = true);
        }

        // ── Corner retention nubs (M2 boss for optional bolt-through) ────
        for (px = [-PAD_W/2 + 5, PAD_W/2 - 5])
        for (pz = [-PAD_H/2 + 5, PAD_H/2 - 5])
            translate([px, -PAD_T/2, pz])
                difference() {
                    cylinder(d = 5, h = PAD_T, center = true);
                    cylinder(d = M2_D, h = PAD_T + 2*e, center = true);
                }
    }
}
