```python
import bpy
import math
from mathutils import Vector

# ------------------------------------------------------------
# Reset
# ------------------------------------------------------------
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

for datablocks in (
    bpy.data.meshes,
    bpy.data.curves,
    bpy.data.materials,
    bpy.data.cameras,
    bpy.data.lights,
):
    pass

scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = 180
scene.frame_set(150)

# ------------------------------------------------------------
# Color management / render
# ------------------------------------------------------------
scene.render.engine = 'BLENDER_EEVEE_NEXT'
scene.render.resolution_x = 1536
scene.render.resolution_y = 1536
scene.render.resolution_percentage = 100
scene.render.image_settings.file_format = 'PNG'
scene.render.film_transparent = False

scene.view_settings.look = 'AgX - Medium High Contrast'
scene.world.color = (0.003, 0.006, 0.014)

# ------------------------------------------------------------
# Materials
# ------------------------------------------------------------
def make_material(name, color, metallic=0.0, roughness=0.45, emission=None):
    mat = bpy.data.materials.new(name)
    mat.diffuse_color = (*color, 1.0)
    mat.use_nodes = True
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    bsdf.inputs["Base Color"].default_value = (*color, 1.0)
    bsdf.inputs["Metallic"].default_value = metallic
    bsdf.inputs["Roughness"].default_value = roughness
    if emission:
        bsdf.inputs["Emission Color"].default_value = (*emission, 1.0)
        bsdf.inputs["Emission Strength"].default_value = 0.25
    return mat

accent = make_material(
    "BENCH — warm amber pieces",
    (1.0, 0.105, 0.018),
    metallic=0.28,
    roughness=0.26,
    emission=(0.28, 0.012, 0.002),
)

back_mat = make_material(
    "Midnight blue background",
    (0.006, 0.014, 0.035),
    metallic=0.0,
    roughness=0.7,
)

edge_mat = make_material(
    "Subtle frame accent",
    (0.18, 0.018, 0.006),
    metallic=0.1,
    roughness=0.35,
)

# ------------------------------------------------------------
# Background slab
# ------------------------------------------------------------
bpy.ops.mesh.primitive_cube_add(location=(0, 0, -0.58))
back = bpy.context.object
back.name = "Deep blue presentation backdrop"
back.dimensions = (10.8, 8.6, 0.28)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
back.data.materials.append(back_mat)

bev = back.modifiers.new("Soft edge", 'BEVEL')
bev.width = 0.16
bev.segments = 5

# Thin graphic bars to give the composition a designed motion-graphics frame
def add_bar(name, location, dimensions, material):
    bpy.ops.mesh.primitive_cube_add(location=location)
    obj = bpy.context.object
    obj.name = name
    obj.dimensions = dimensions
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    obj.data.materials.append(material)
    bevel = obj.modifiers.new("Rounded ends", 'BEVEL')
    bevel.width = min(dimensions) * 0.22
    bevel.segments = 4
    return obj

add_bar("Top amber rule", (0, 3.40, -0.37), (5.6, 0.025, 0.025), edge_mat)
add_bar("Bottom amber rule", (0, -3.40, -0.37), (5.6, 0.025, 0.025), edge_mat)

# ------------------------------------------------------------
# Text source mesh
# ------------------------------------------------------------
bpy.ops.object.text_add(location=(0, -0.03, 0.0), rotation=(0, 0, 0))
text_obj = bpy.context.object
text_obj.name = "BENCH target text geometry"

text_curve = text_obj.data
text_curve.body = "BENCH"
text_curve.align_x = 'CENTER'
text_curve.align_y = 'CENTER'
text_curve.size = 2.05
text_curve.space_character = 1.02
text_curve.extrude = 0.075
text_curve.bevel_depth = 0.012
text_curve.bevel_resolution = 2
text_curve.resolution_u = 10

# Convert to a real mesh: this is the target geometry sampled by Geometry Nodes.
bpy.context.view_layer.objects.active = text_obj
text_obj.select_set(True)
bpy.ops.object.convert(target='MESH')
text_obj = bpy.context.object
text_obj.name = "BENCH — sampled target mesh"

# Normalize width slightly while preserving readable proportions.
scale_x = 6.55 / text_obj.dimensions.x
text_obj.scale.x *= scale_x
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)

# ------------------------------------------------------------
# Geometry Nodes: sampled text points -> scattered animated pieces
# ------------------------------------------------------------
ng = bpy.data.node_groups.new("BENCH procedural scatter assembly", 'GeometryNodeTree')

ng.interface.new_socket(
    name="Geometry",
    in_out='INPUT',
    socket_type='NodeSocketGeometry'
)
progress_socket = ng.interface.new_socket(
    name="Assembly Progress",
    in_out='INPUT',
    socket_type='NodeSocketFloat'
)
progress_socket.default_value = 0.0
progress_socket.min_value = 0.0
progress_socket.max_value = 1.0
ng.interface.new_socket(
    name="Geometry",
    in_out='OUTPUT',
    socket_type='NodeSocketGeometry'
)

nodes = ng.nodes
links = ng.links

inp = nodes.new("NodeGroupInput")
inp.name = "Text target geometry"
inp.label = "TARGET TEXT"
inp.location = (-1000, 120)

out = nodes.new("NodeGroupOutput")
out.location = (780, 120)

distribute = nodes.new("GeometryNodeDistributePointsOnFaces")
distribute.name = "Sample points across text faces"
distribute.label = "SAMPLE TEXT SURFACE"
distribute.location = (-780, 120)
distribute.distribute_method = 'RANDOM'
distribute.inputs["Density"].default_value = 32.0
distribute.inputs["Distance Min"].default_value = 0.0

position = nodes.new("GeometryNodeInputPosition")
position.location = (-780, -350)

separate = nodes.new("ShaderNodeSeparateXYZ")
separate.location = (-580, -350)

# Normalize x position into a left-to-right wipe threshold.
map_x = nodes.new("ShaderNodeMapRange")
map_x.location = (-370, -350)
map_x.clamp = True
map_x.inputs["From Min"].default_value = -3.35
map_x.inputs["From Max"].default_value = 3.35
map_x.inputs["To Min"].default_value = 0.03
map_x.inputs["To Max"].default_value = 0.78

subtract = nodes.new("ShaderNodeMath")
subtract.operation = 'SUBTRACT'
subtract.location = (-140, -230)

divide = nodes.new("ShaderNodeMath")
divide.operation = 'DIVIDE'
divide.location = (40, -230)
divide.inputs[1].default_value = 0.25

clamp = nodes.new("ShaderNodeClamp")
clamp.location = (210, -230)
clamp.inputs["Min"].default_value = 0.0
clamp.inputs["Max"].default_value = 1.0

# Smoothstep easing: t*t*(3-2*t)
multiply_a = nodes.new("ShaderNodeMath")
multiply_a.operation = 'MULTIPLY'
multiply_a.location = (390, -230)

two_t = nodes.new("ShaderNodeMath")
two_t.operation = 'MULTIPLY'
two_t.location = (390, -330)
two_t.inputs[1].default_value = 2.0

three_minus = nodes.new("ShaderNodeMath")
three_minus.operation = 'SUBTRACT'
three_minus.location = (560, -330)
three_minus.inputs[0].default_value = 3.0

ease = nodes.new("ShaderNodeMath")
ease.operation = 'MULTIPLY'
ease.location = (560, -230)

one_minus = nodes.new("ShaderNodeMath")
one_minus.operation = 'SUBTRACT'
one_minus.location = (730, -230)
one_minus.inputs[0].default_value = 1.0

random_offset = nodes.new("FunctionNodeRandomValue")
random_offset.data_type = 'FLOAT_VECTOR'
random_offset.location = (-100, 250)
random_offset.inputs["Min"].default_value = (-1.35, -1.18, -1.10)
random_offset.inputs["Max"].default_value = (1.35, 1.18, 1.10)
random_offset.inputs["ID"].default_value = 17

offset_scale = nodes.new("ShaderNodeVectorMath")
offset_scale.operation = 'SCALE'
offset_scale.location = (420, 250)

set_position = nodes.new("GeometryNodeSetPosition")
set_position.name = "Procedural scatter offset"
set_position.label = "SCATTER → ASSEMBLE"
set_position.location = (0, 120)

piece = nodes.new("GeometryNodeMeshIcoSphere")
piece.name = "Small instanced piece"
piece.label = "AMBER MICRO-PIECE"
piece.location = (-20, -580)
piece.inputs["Radius"].default_value = 0.048
piece.inputs["Subdivisions"].default_value = 1

instance = nodes.new("GeometryNodeInstanceOnPoints")
instance.name = "Instance pieces on sampled targets"
instance.location = (250, 120)

# Secondary settle: scale pop peaks during each point's arrival.
settle_pi = nodes.new("ShaderNodeMath")
settle_pi.operation = 'MULTIPLY'
settle_pi.location = (200, -520)
settle_pi.inputs[1].default_value = math.pi

settle_sine = nodes.new("ShaderNodeMath")
settle_sine.operation = 'SINE'
settle_sine.location = (380, -520)

settle_amp = nodes.new("ShaderNodeMath")
settle_amp.operation = 'MULTIPLY'
settle_amp.location = (550, -520)
settle_amp.inputs[1].default_value = 0.18

settle_add = nodes.new("ShaderNodeMath")
settle_add.operation = 'ADD'
settle_add.location = (710, -520)
settle_add.inputs[0].default_value = 0.88

combine_scale = nodes.new("ShaderNodeCombineXYZ")
combine_scale.location = (720, -90)

set_material = nodes.new("GeometryNodeSetMaterial")
set_material.location = (560, 120)
set_material.inputs["Material"].default_value = accent

# Main geometry links
links.new(inp.outputs["Geometry"], distribute.inputs["Mesh"])
links.new(distribute.outputs["Mesh"], set_position.inputs["Geometry"])
links.new(position.outputs["Position"], separate.inputs["Vector"])
links.new(separate.outputs["X"], map_x.inputs["Value"])
links.new(map_x.outputs["Result"], subtract.inputs[1])
links.new(inp.outputs["Assembly Progress"], subtract.inputs[0])
links.new(subtract.outputs[0], divide.inputs[0])
links.new(divide.outputs[0], clamp.inputs["Value"])
links.new(clamp.outputs["Result"], multiply_a.inputs[0])
links.new(clamp.outputs["Result"], two_t.inputs[0])
links.new(three_minus.outputs[0], ease.inputs[1])
links.new(two_t.outputs[0], three_minus.inputs[1])
links.new(multiply_a.outputs[0], ease.inputs[0])
links.new(ease.outputs[0], one_minus.inputs[1])

links.new(random_offset.outputs["Value"], offset_scale.inputs[0])
links.new(one_minus.outputs[0], offset_scale.inputs[3])
links.new(offset_scale.outputs["Vector"], set_position.inputs["Offset"])

links.new(set_position.outputs["Geometry"], instance.inputs["Points"])
links.new(piece.outputs["Mesh"], instance.inputs["Instance"])

# Scale pop driven by the eased local completion.
links.new(ease.outputs[0], settle_pi.inputs[0])
links.new(settle_pi.outputs[0], settle_sine.inputs[0])
links.new(settle_sine.outputs[0], settle_amp.inputs[0])
links.new(settle_amp.outputs[0], settle_add.inputs[1])
links.new(settle_add.outputs[0], combine_scale.inputs["X"])
links.new(settle_add.outputs[0], combine_scale.inputs["Y"])
links.new(settle_add.outputs[0], combine_scale.inputs["Z"])
links.new(combine_scale.outputs["Vector"], instance.inputs["Scale"])

links.new(instance.outputs["Instances"], set_material.inputs["Geometry"])
links.new(set_material.outputs["Geometry"], out.inputs["Geometry"])

# Add modifier and animate the single assembly factor.
mod = text_obj.modifiers.new("Geometry Nodes — animated BENCH assembly", 'NODES')
mod.node_group = ng

progress_identifier = None
for item in ng.interface.items_tree:
    if getattr(item, "item_type", None) == 'SOCKET':
        if item.in_out == 'INPUT' and item.name == "Assembly Progress":
            progress_identifier = item.identifier
            break

if progress_identifier:
    mod[progress_identifier] = 0.0
    mod.keyframe_insert(data_path=f'["{progress_identifier}"]', frame=1)
    mod[progress_identifier] = 1.0
    mod.keyframe_insert(data_path=f'["{progress_identifier}"]', frame=118)
    # Hold assembled through the final frames.
    mod[progress_identifier] = 1.0
    mod.keyframe_insert(data_path=f'["{progress_identifier}"]', frame=180)

    action = text_obj.animation_data.action if text_obj.animation_data else None
    if action:
        for fc in action.fcurves:
            for kp in fc.keyframe_points:
                kp.interpolation = 'BEZIER'

# ------------------------------------------------------------
# Camera
# ------------------------------------------------------------
bpy.ops.object.camera_add(location=(0.0, 0.0, 11.5))
camera = bpy.context.object
camera.name = "Presentation camera — BENCH"
camera.data.type = 'ORTHO'
camera.data.ortho_scale = 8.25
camera.data.lens = 52
camera.rotation_euler = (0.0, 0.0, 0.0)
scene.camera = camera

# Camera points down -Z by default with zero rotation.
# Slightly tilt the camera for a tiny amount of dimensionality while retaining legibility.
camera.rotation_euler = (0.0, 0.0, 0.0)

# ------------------------------------------------------------
# Lighting
# ------------------------------------------------------------
def add_area(name, location, energy, size, color):
    bpy.ops.object.light_add(type='AREA', location=location)
    lamp = bpy.context.object
    lamp.name = name
    lamp.data.energy = energy
    lamp.data.shape = 'DISK'
    lamp.data.size = size
    lamp.data.color = color
    lamp.rotation_euler = (0, 0, 0)
    return lamp

# Area lights face downward along -Z.
add_area("Large soft key", (0.0, 0.5, 5.8), 900.0, 5.0, (1.0, 0.28, 0.12))
add_area("Cool fill", (-4.5, 1.2, 3.5), 650.0, 4.0, (0.08, 0.18, 1.0))
add_area("Warm rim", (4.5, -1.8, 2.8), 800.0, 3.0, (1.0, 0.08, 0.025))

# ------------------------------------------------------------
# Organization and final state
# ------------------------------------------------------------
# Keep target mesh active and visible: the GN modifier is the actual animated scene.
text_obj.select_set(True)
bpy.context.view_layer.objects.active = text_obj

# Make the completed word the saved presentation frame.
scene.frame_set(150)

# Favor clean viewport display.
for obj in bpy.context.scene.objects:
    if obj.type == 'MESH':
        obj.display_type = 'TEXTURED'

# Save complete scene.
bpy.ops.wm.save_as_mainfile(filepath="/tmp/bench_title_reveal.blend")
```